Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ぬるぬる動かせ! Riveでアニメーション実装🐾

Avatar for Kuno Ayana Kuno Ayana
September 04, 2025

ぬるぬる動かせ! Riveでアニメーション実装🐾

Avatar for Kuno Ayana

Kuno Ayana

September 04, 2025
Tweet

More Decks by Kuno Ayana

Other Decks in Programming

Transcript

  1. IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM AnimationController サンプル import 'dart:ui'; import 'package:flutter/material.dart'; class

    LikeButton extends StatefulWidget { final 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(
  2. IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM 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, ), ), ], ), ), ); } }
  3. 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, ), ), ], ), ), ); } } IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM ΞΠίϯ֦େॖখͱϦϯάͷ޿͕ΓͷͨΊʹ "OJNBUJPO$POUSPMMFSΛͭ༻ҙͯ͠ʜ
  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, ), ), ], ), ), ); } } IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM ΞΠίϯ֦େॖখͱϦϯάͷ޿͕ΓͷͨΊʹ "OJNBUJPO$POUSPMMFSΛͭ༻ҙͯ͠ʜ ͦΕͧΕͷ஋Λܭࢉͯ͠ʜ
  5. 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, ), ), ], ), ), ); } } IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM ΞΠίϯ֦େॖখͱϦϯάͷ޿͕ΓͷͨΊʹ "OJNBUJPO$POUSPMMFSΛͭ༻ҙͯ͠ʜ ͦΕͧΕͷ஋Λܭࢉͯ͠ʜ ͦΕͧΕ4UBUFΛ؅ཧͯ͠ʜ
  6. 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, ), ), ], ), ), ); } } IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM ΞΠίϯ֦େॖখͱϦϯάͷ޿͕ΓͷͨΊʹ "OJNBUJPO$POUSPMMFSΛͭ༻ҙͯ͠ʜ ͦΕͧΕͷ஋Λܭࢉͯ͠ʜ ͦΕͧΕ4UBUFΛ؅ཧͯ͠ʜ ΍΂ͬEJTQPTF๨Εͯͨʜ
  7. 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, ), ), ], ), ), ); } } IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM ちょ〜たいへん!
  8. 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, ), ), ], ), ), ); } } IUUQTHJUIVCDPNLOPB fl VUUFS@SJWF@TBNQMFQVMM Rive ͦ͜Ͱొ৔ʂ
  9. _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
  10. _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
  11. _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
  12. 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; }
  13. 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; }
  14. 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; }
  15. _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(
  16. _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(
  17. import 'dart:ui'; import 'package:flutter/material.dart'; class LikeButton extends StatefulWidget { final

    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(); } }
  18. import 'dart:ui'; import 'package:flutter/material.dart'; class LikeButton extends StatefulWidget { final

    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(); } } レビューしやすい! デザイナさんとつくれる!