Flutter 12 实现双击屏幕显示点赞爱心多种动画(AnimationIcon)效果

本文主要是使用Flutter封装一个双击屏幕显示点赞爱心UI效果,并实现了爱心Icon 透明度、缩放、旋转、渐变等动画效果。

实现效果:

实现逻辑:

1、封装FavoriteGesture(爱心手势)实现双击屏幕显示爱心Icon;

2、封装FavoriteAnimationIcon(爱心Icon)实现双击屏幕显示爱心Icon,快速双击时同时显示多个爱心Icon;

3、给FavoriteAnimationIcon增加透明度淡入淡出动画效果;

4、给FavoriteAnimationIcon增加缩放动画效果;

5、给FavoriteAnimationIcon增加旋转动画效果;

7、给FavoriteAnimationIcon增加渐变动画效果;

一、封装FavoriteGesture爱心手势

1)Stack实现多界面堆叠

实现爱心Icon显示在视频界面上层,视频界面由上层child传入,使用Stack实现多界面堆叠;

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        Icon(Icons.favorite, size: widget.size, color: Colors.redAccent)
      ],
    );
  }
}

2)GestureDetector监听双击事件

使用GestureDetector监听屏幕双击事件和点击的坐标,使用Positioned将Icon显示到点击坐标的位置;当用户双击屏幕时,显示爱心Icon,延迟600毫秒,爱心Icon消失。

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {
  final GlobalKey _key = GlobalKey();

  bool inFavorite = false;

  // temp表示最近的一次双击坐标
  Offset temp = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        key: _key,
        onDoubleTapDown: (details) {
          temp = details.globalPosition;
        },
        onDoubleTap: () {
          setState(() {
            inFavorite = true;
          });
          Future.delayed(const Duration(milliseconds: 600), () {
            setState(() {
              inFavorite = false;
            });
          });
        },
        child: Stack(
          children: [
            widget.child,
            if (inFavorite)
              Positioned(
                  top: temp.dy - widget.size / 2,
                  left: temp.dx - widget.size / 2,
                  child: Icon(Icons.favorite, size: widget.size, color: Colors.redAccent)),
          ],
        ));
  }
}

3)RenderBox 实现屏幕坐标转换本地坐标

onDoubleTapDown: (details) {temp = details.globalPosition;} 获取到的点击坐标是屏幕的坐标,需要转换成Icon的父布局Stack的坐标,通过RenderBox 来实现屏幕坐标转换本地坐标。

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {
  final GlobalKey _key = GlobalKey();

  bool inFavorite = false;

  // temp表示最近的一次双击坐标
  Offset temp = Offset.zero;

  Offset _globalToLocal(Offset global) {
    RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
    return renderBox.globalToLocal(global);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        key: _key,
        onDoubleTapDown: (details) {
          temp = _globalToLocal(details.globalPosition);
        },
        onDoubleTap: () {
          setState(() {
            inFavorite = true;
          });
          Future.delayed(const Duration(milliseconds: 600), () {
            setState(() {
              inFavorite = false;
            });
          });
        },
        child: Stack(
          children: [
            widget.child,
            if (inFavorite)
              Positioned(
                  top: temp.dy - widget.size / 2,
                  left: temp.dx - widget.size / 2,
                  child: Icon(Icons.favorite, size: widget.size, color: Colors.redAccent)),
          ],
        ));
  }
}

实现效果如下:

二、封装FavoriteAnimationIcon 爱心Icon

1)封装FavoriteAnimationIcon

封装FavoriteAnimationIcon,将Icon坐标、大小、动效等统一封装起来,并暴露动效结束的回调接口给上层,便于清除过期的爱心Icon。

Dart 复制代码
/*年轻人,只管向前看,不要管自暴自弃者的话*/

import 'package:flutter/material.dart';

///create by itz on 2024-10-24 14:19
///desc :
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    Future.delayed(const Duration(milliseconds: 600), () {
      widget.onAnimationComplete?.call();
    });
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    return Positioned(
        top: widget.position.dy - widget.size / 2, left: widget.position.dx - widget.size / 2, child: content);
  }
}

2)快速双击时,同时显示多个爱心Icon。

使用list记录双击的坐标位置,实现快速双击屏幕时,同时显示多个爱心Icon;多个爱心Icon,使用Stack实现多爱心Icon堆叠效果。

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {
  final GlobalKey _key = GlobalKey();

  // 保存当前需要展示的icon
  List<Offset> iconOffsets = [];

  // temp表示最近的一次双击坐标
  Offset temp = Offset.zero;

  Offset _globalToLocal(Offset global) {
    RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
    return renderBox.globalToLocal(global);
  }

  @override
  Widget build(BuildContext context) {
    var iconStack = Stack(
        children: iconOffsets
            .map((e) => FavoriteAnimationIcon(
                  position: e,
                  size: widget.size,
                  onAnimationComplete: () {
                    setState(() => iconOffsets.remove(e));
                  },
                ))
            .toList());

    return GestureDetector(
        key: _key,
        onDoubleTapDown: (details) {
          temp = _globalToLocal(details.globalPosition);
        },
        onDoubleTap: () {
          // 添加坐标到集合中,触发一次重绘制。根据坐标集合来在不同的坐标上渲染出icon
          setState(() => iconOffsets.add(temp));
        },
        child: Stack(
          children: [
            widget.child,
            iconStack,
          ],
        ));
  }
}

实现效果如下:

三、透明度淡入淡出动画效果

1)Animation 动画

本节使用Animation forward 正向播放来实现淡入淡出动画效果。

2)Opacity Widget

在 Flutter 中,Opacity Widget 用于控制其子部件(child widget)的不透明度。通过设置 Opacity Widget 的 opacity 属性为一个介于 0.0(完全透明)和 1.0(完全不透明)之间的值,可以调整子部件的可见度。

3)淡入淡出动画设计

我们将爱心淡入、显示、淡出时间占比分别设计为:淡入10%、显示70%、淡出20%;这个可以自由设置。

以Icon显示但消失时间1S来计算:

淡入10% = 100ms内 实现 opacity 属性从 0.0(完全透明)和 1.0(完全不透明)的值。

显示70% = 700ms内 实现 opacity 属性值 一直等于1(完全不透明)。

淡入20% = 200ms内 实现 opacity 属性从 0.0(完全透明)和 1.0(完全不透明)的值。

Dart 复制代码
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    return Positioned(
        top: widget.position.dy - widget.size / 2,
        left: widget.position.dx - widget.size / 2,
        child: Opacity(opacity: opacity, child: content));
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

四、缩放动画效果

Transform.scale Widget

在 Flutter 中,Transform.scale Widget 用于按比例缩放其子部件(child widget)。通过设置 Transform.scale 的 scale 属性为一个倍数,可以调整子部件的缩放比例。

Icon缩放时间同上面的 淡入淡出动画设计

Dart 复制代码
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    var child = Transform.scale(scale: scale, alignment: Alignment.bottomCenter, child: content);

    return Positioned(
        top: widget.position.dy - widget.size / 2,
        left: widget.position.dx - widget.size / 2,
        child: Opacity(opacity: opacity, child: child));
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  // 需要计算缩放尺寸的占比
  double get scale {
    if (value < appearValue) {
      // 处于出现阶段(从1.1到1 缩小的过程)
      return 1 + appearValue - value;
    }

    if (value < dismissValue) {
      // 处于正常展示阶段
      return 1;
    }

    // 处于消失放大阶段
    return 1 + (value - dismissValue) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

五、旋转动画效果

Transform.rotate Widget

在 Flutter 中,Transform.rotate Widget 用于旋转其子部件(child widget)。通过设置 Transform.rotate 的 angle 属性为一个角度值,可以使子部件绕中心旋转指定的角度。

Dart 复制代码
import 'dart:math';

import 'package:flutter/material.dart';

///create by itz on 2024-10-24 14:19
///desc :
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  final double angle = pi / 10 * (2 * Random().nextDouble() - 1);

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    // 旋转
    var childRotate = Transform.rotate(angle: angle, child: content);

    // 缩放
    var childScale = Transform.scale(scale: scale, alignment: Alignment.bottomCenter, child: childRotate);

    // 透明度
    var childOpacity = Opacity(opacity: opacity, child: childScale);

    return Positioned(
        top: widget.position.dy - widget.size / 2, left: widget.position.dx - widget.size / 2, child: childOpacity);
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  // 需要计算缩放尺寸的占比
  double get scale {
    if (value < appearValue) {
      // 处于出现阶段(从1.1到1 缩小的过程)
      return 1 + appearValue - value;
    }

    if (value < dismissValue) {
      // 处于正常展示阶段
      return 1;
    }

    // 处于消失放大阶段
    return 1 + (value - dismissValue) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

六、渐变动画效果

ShaderMask Widget

在 Flutter 中,ShaderMask Widget 用于将子部件应用一个着色器效果,可以用来创建各种视觉效果,比如颜色遮罩、渐变遮罩等。

Dart 复制代码
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  final double angle = pi / 10 * (2 * Random().nextDouble() - 1);

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    // 缩放
    var childScale = Transform.scale(scale: scale, alignment: Alignment.bottomCenter, child: content);

    // 旋转
    var childRotate = Transform.rotate(angle: angle, child: childScale);

    // 渐变
    var childShaderMask = ShaderMask(
        blendMode: BlendMode.srcATop,
        shaderCallback: (Rect bounds) => RadialGradient(
                colors: const [Color(0xFFEE6E6E), Color(0xFFF03F3F)],
                center: Alignment.topLeft.add(const Alignment(0.66, 0.66)))
            .createShader(bounds),
        child: childRotate);

    // 透明度
    var childOpacity = Opacity(opacity: opacity, child: childShaderMask);

    return Positioned(
        top: widget.position.dy - widget.size / 2, left: widget.position.dx - widget.size / 2, child: childOpacity);
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  // 需要计算缩放尺寸的占比
  double get scale {
    if (value < appearValue) {
      // 处于出现阶段(从1.1到1 缩小的过程)
      return 1 + appearValue - value;
    }

    if (value < dismissValue) {
      // 处于正常展示阶段
      return 1;
    }

    // 处于消失放大阶段
    return 1 + (value - dismissValue) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

相关推荐
醉过才知酒浓11 小时前
Flutter Getx 的页面传参
flutter
火柴就是我1 天前
flutter 之真手势冲突处理
android·flutter
Speed1231 天前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间1 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭1 天前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
玲珑Felone1 天前
从flutter源码看其渲染机制
android·flutter
ALLIN2 天前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei2 天前
Flutter 国际化
flutter
Dabei2 天前
Flutter MQTT 通信文档
flutter
Dabei2 天前
Flutter 中实现 TCP 通信
flutter