仿抖音點贊手勢,單擊暫停,雙擊點贊,可連續點擊添加多個愛心,特色以下segmentfault
一個罩在child上的stack層,雙擊後根據座標添加目標愛心Widget,愛心Widget在出現時會播放動畫,用座標做爲key,動畫結束後移除已經消失的愛心。app
什麼都不依賴,能夠直接複製使用。dom
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<TikTokVideoGesture> { GlobalKey _key = GlobalKey(); // 內部轉換座標點 Offset _p(Offset p) { RenderBox getBox = _key.currentContext.findRenderObject(); return getBox.globalToLocal(p); } List<Offset> icons = []; bool canAddFavorite = false; bool justAddFavorite = false; Timer timer; @override Widget build(BuildContext context) { var iconStack = Stack( children: icons .map<Widget>( (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>[ 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<TikTokFavoriteAnimationIcon> 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, ); } }
做者:馬嘉倫
日期:2020/01/31
平臺:Segmentfault獨家,勿轉載 async
個人其餘文章:
【開發經驗】淺談flutter的優勢與缺點
【Flutter工具】fmaker:自動生成倍率切圖/自動更換App圖標
【開發經驗】在Flutter中使用dart的單例模式ide