简介
仿抖音点赞手势,单击暂停,双击点赞,可连续点击添加多个爱心,特点如下
全部效果为代码绘制(爱心图标来自 Icon的图标)套上在目标外即可使用提供单击与点赞的回调建议复制代码使用,动画可按需修改没有之外的依赖项,可复制使用(懒得发pub)基本原理
一个罩在child上的stack层,双击后根据坐标添加目标爱心,爱心在出现时会播放动画,用坐标作为key,动画结束后移除已经消失的爱心。
全部代码
什么都不依赖,可以直接复制使用。
import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; /// 视频手势封装 /// 单击:暂停 /// 双击:点赞,双击后再次单击也是增加点赞爱心 class TikTokVideoGesture extends StatefulWidget { const TikTokVideoGesture({ Key key, @required this.child, this.onAddFavorite, this.onSingleTap, }) : super(key: key); final Function onAddFavorite; final Function onSingleTap; final Widget child; @override _TikTokVideoGestureState createState() => _TikTokVideoGestureState(); } class _TikTokVideoGestureState extends State{ GlobalKey _key = GlobalKey(); // 内部转换坐标点 Offset _p(Offset p) { RenderBox getBox = _key.currentContext.findRenderObject(); return getBox.globalToLocal(p); } List icons = []; bool canAddFavorite = false; bool justAddFavorite = false; Timer timer; @override Widget build(BuildContext context) { var iconStack = Stack( children: icons .map ( (p) => TikTokFavoriteAnimationIcon( key: Key(p.toString()), position: p, onAnimationComplete: () { icons.remove(p); }, ), ) .toList(), ); return GestureDetector( key: _key, onTapDown: (detail) { setState(() { if (canAddFavorite) { print('添加爱心,当前爱心数量:${icons.length}'); icons.add(_p(detail.globalPosition)); widget.onAddFavorite?.call(); justAddFavorite = true; } else { justAddFavorite = false; } }); }, onTapUp: (detail) { timer?.cancel(); var delay = canAddFavorite ? 1200 : 600; timer = Timer(Duration(milliseconds: delay), () { canAddFavorite = false; timer = null; if (!justAddFavorite) { widget.onSingleTap?.call(); } }); canAddFavorite = true; }, onTapCancel: () { print('onTapCancel'); }, child: Stack( children: [ widget.child, iconStack, ], ), ); } } class TikTokFavoriteAnimationIcon extends StatefulWidget { final Offset position; final double size; final Function onAnimationComplete; const TikTokFavoriteAnimationIcon({ Key key, this.onAnimationComplete, this.position, this.size: 100, }) : super(key: key); @override _TikTokFavoriteAnimationIconState createState() => _TikTokFavoriteAnimationIconState(); } class _TikTokFavoriteAnimationIconState extends State with TickerProviderStateMixin { AnimationController _animationController; @override void dispose() { _animationController?.dispose(); super.dispose(); } @override void didChangeDependencies() { print('didChangeDependencies'); super.didChangeDependencies(); } @override void initState() { _animationController = AnimationController( lowerBound: 0, upperBound: 1, duration: Duration(milliseconds: 1600), vsync: this, ); _animationController.addListener(() { setState(() {}); }); startAnimation(); super.initState(); } startAnimation() async { await _animationController.forward(); widget.onAnimationComplete?.call(); } double rotate = pi / 10.0 * (2 * Random().nextDouble() - 1); double get value => _animationController?.value; double appearDuration = 0.1; double dismissDuration = 0.8; double get opa { if (value < appearDuration) { return 0.99 / appearDuration * value; } if (value < dismissDuration) { return 0.99; } var res = 0.99 - (value - dismissDuration) / (1 - dismissDuration); return res < 0 ? 0 : res; } double get scale { if (value < appearDuration) { return 1 + appearDuration - value; } if (value < dismissDuration) { return 1; } return (value - dismissDuration) / (1 - dismissDuration) + 1; } @override Widget build(BuildContext context) { Widget content = Icon( Icons.favorite, size: widget.size, color: Colors.redAccent, ); content = ShaderMask( child: content, blendMode: BlendMode.srcATop, shaderCallback: (Rect bounds) => RadialGradient( center: Alignment.topLeft.add(Alignment(0.66, 0.66)), colors: [ Color(0xffEF6F6F), Color(0xffF03E3E), ], ).createShader(bounds), ); Widget body = Transform.rotate( angle: rotate, child: Opacity( opacity: opa, child: Transform.scale( alignment: Alignment.bottomCenter, scale: scale, child: content, ), ), ); return widget.position == null ? Container() : Positioned( left: widget.position.dx - widget.size / 2, top: widget.position.dy - widget.size / 2, child: body, ); } }
结语