美文网首页
Flutter特效--给控件钻石般的微光

Flutter特效--给控件钻石般的微光

作者: karl_wei | 来源:发表于2021-08-22 14:33 被阅读0次

    最近迷上了《扫黑风暴》,崇拜起了李成阳,特此周末用flutter为大家献上一个保熟的瓜。🍉 🐶

    效果图

    这瓜保熟

    使用方法

    Shimmer(
      baseColor: const Color(0x08ffffff), // 背景颜色
      highlightColor: Colors.white, // 高光的颜色
      loop: 2, // 闪烁循环次数,不传默认一直循环
      child: Image.asset('assets/images/watermelon.png',width: 325, height: 260, fit: BoxFit.contain),
    )
    

    实现原理

    ① 特效控件分为两层:底层显示调用方传入的控件;上层覆盖一层渐变着色器。
    ② 启动动画,根据动画的进度,对渐变着色器的区域进行绘制,当区域变大变小时,着色器高光的地方也在相应进行偏移。
    ③ 同时着色器不能超出底层控件的绘制范围,底层控件的形状是不规则的,渐变层不能超出底层控件的layer对象。这样才能实现完全贴合 底层控件形状 的微光闪烁。

    • 控件分层显示
    @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            // 底层控件
            widget.child,
            // 覆盖闪烁微光
            AnimatedBuilder(
              animation: _controller,
              child: widget.child,
              builder: (BuildContext context, Widget? child) => _Shimmer(
                child: child,
                percent: _controller.value,
                direction: widget.direction,
                gradient: widget.gradient,
              ),
            )
          ],
        );
    
    • 开启动画
      late AnimationController _controller;
      int _count = 0;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(vsync: this, duration: widget.duration)
          ..addStatusListener((AnimationStatus status) {
            if (status != AnimationStatus.completed) {
              return;
            }
            _count++;
            if (widget.loop != 0 && _count < widget.loop) {
              _controller.forward(from: 0.0);
            }
          });
    
        if (widget.loop == 0) {
          _controller.repeat();
        } else {
          _controller.forward();
        }
      }
    
    • 重点:着色器该如何绘制,又该如何通过AnimationController的进度进行偏移?由于着色器不能超出底层控件的绘制范围,所以必须拿到底层控件的绘制上下文【即 PaintingContext】,调用其pushLayer方法,让引擎把着色器绘制上去。
      需要用到PaintingContext,自然就需要去管理RenderObject,所以着色器的编写使用RenderProxyBox进行计算并绘制出layer对象,计算的过程根据上面的AnimationController的进度进行计算。
    class _ShimmerFilter extends RenderProxyBox {
      ShimmerDirection _direction;
      Gradient _gradient;
      double _percent;
    
      _ShimmerFilter(this._percent, this._direction, this._gradient);
    
      @override
      ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;
    
      set percent(double newValue) {
        if (newValue != _percent) {
          _percent = newValue;
          markNeedsPaint();
        }
      }
    
      set gradient(Gradient newValue) {
        if (newValue != _gradient) {
          _gradient = newValue;
          markNeedsPaint();
        }
      }
    
      set direction(ShimmerDirection newDirection) {
        if (newDirection != _direction) {
          _direction = newDirection;
          markNeedsLayout();
        }
      }
    
      @override
      void paint(PaintingContext context, Offset offset) {
        if (child != null) {
          final double width = child!.size.width;
          final double height = child!.size.height;
          Rect rect;
          double dx, dy;
          if (_direction == ShimmerDirection.rtl) {
            dx = _offset(width, -width, _percent);
            dy = 0.0;
            rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
          } else if (_direction == ShimmerDirection.ttb) {
            dx = 0.0;
            dy = _offset(-height, height, _percent);
            rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
          } else if (_direction == ShimmerDirection.btt) {
            dx = 0.0;
            dy = _offset(height, -height, _percent);
            rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
          } else {
            dx = _offset(-width, width, _percent);
            dy = 0.0;
            rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
          }
          layer ??= ShaderMaskLayer();
          layer!
            ..shader = _gradient.createShader(rect)
            ..maskRect = offset & size
            ..blendMode = BlendMode.srcIn;
          context.pushLayer(layer!, super.paint, offset);
        } else {
          layer = null;
        }
      }
    
      double _offset(double start, double end, double percent) {
        return start + (end - start) * percent;
      }
    }
    

    Render对象绘制出来后,需要封装成widget使用,由于是单一组件,用SingleChildRenderObjectWidget即可。

    class _Shimmer extends SingleChildRenderObjectWidget {
      @override
      _ShimmerFilter createRenderObject(BuildContext context) {
        return _ShimmerFilter(percent, direction, gradient);
      }
      @override
      void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) {
        shimmer.percent = percent;
        shimmer.gradient = gradient;
        shimmer.direction = direction;
      }
    }
    

    写在最后

    1. 这种闪烁动画,应用场景多种多样。可以作为对重要视图的着重显示,例如:勋章;也可以作为加载中骨架屏的加载动画。自己灵活使用即可。
    2. 作为一个大前端开发者,我希望把UI尽善尽美的展现给用户;此时你不仅需要一个集能力、审美、高标准于一体的设计师配合,更需要自己对所写界面有着极高的追求。而Flutter作为一个UI框架,玩到最后其实就是特效动画的高性能编写,这势必离不开其绘制原理,不要停留在widget、element的学习,Render、layer甚至再底层的C++才是我们学习路径。
    3. 参考文档:
      https://api.flutter-io.cn/flutter/rendering/PaintingContext-class.html
      https://www.cnblogs.com/lxlx1798/articles/11219684.html
      https://github.com/hnvn/flutter_shimmer
      小弟班门弄斧,希望能一起学习进步!!!

    相关文章

      网友评论

          本文标题:Flutter特效--给控件钻石般的微光

          本文链接:https://www.haomeiwen.com/subject/xficiltx.html