Code前端首页关于Code前端联系我们

Flutter实现底部扩散模糊动画:跳转页面

terry 2年前 (2023-09-23) 阅读数 69 #移动小程序

在阅读本文之前,你必须对Flutter有一定的了解,包括生命周期、高斯模糊、动画、MediaQuery等相关知识。当然,所有内容都可以通过搜索找到~

效果图:Flutter 实现底部扩散模糊动画:跳转页面

交互过程主要分为以下三个步骤:

  • 点击加号,将高斯的模糊效果从加号处一圈扩散开来标志位置;
  • 动作项一个接一个地出现,并带有一些动画效果;
  • 单击“X”或空格或系统返回键,背景将缩小到圆圈中加号的位置。

完整的demo和组件已经上传到项目中,路过请给个star~

  • ripple_backdrop_animate_route
  • OpenJMU/lib/pages/home/AddButtonPage.dart
  • OpenJMU/lib/OpenJMU MainPage .dart

前提条件

想要达到效果,首先要明确几个前提条件:

  • 路由必须是透明的,否则高斯模糊就无法做到之前的路由;
  • 根据life循环中,启动动画必须在第一个Build之后立即完成,不能在InitCies中完成 否则会有上下文。 问题 为空或在错误的时间开始;
  • 关闭动画必须在pop()之前完成,否则widget断开连接ⓙ'=toto false

实现过程

以下具体的实现过程将结合上述条件进行说明。

透明跳转路由

网上有很多透明路由的例子,包括Fafa路由,其中​​也包括透明路由。这里就不详细说了,直接贴代码。

class TransparentRoute extends PageRoute<void> {
    TransparentRoute({
        @required this.builder,
        RouteSettings settings,
    })  : assert(builder != null),
                super(settings: settings, fullscreenDialog: false);

    final WidgetBuilder builder;

    @override
    bool get opaque => false;
    @override
    Color get barrierColor => null;
    @override
    String get barrierLabel => null;
    @override
    bool get maintainState => true;
    @override
    /// 这里时长设置为0,是因为我们的布局一开始
    /// 并不包含任何内容,所以直接砍掉跳转时间。
    Duration get transitionDuration => Duration.zero;

    @override
    Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
        final result = builder(context);
        return Semantics(
            scopesRoute: true,
            explicitChildNodes: true,
            child: result,
        );
    }
}
复制代码

施工完成后,只需按即可。

Navigator.of(context).push(TransparentRoute(
    builder: (context) => AddingButtonPage(),
));
复制代码

扩散动画

如果要在小部件中实现奔跑动画,必须先添加

并声明TickerProviderStateMixin ler动画(动画)单独。

class _DemoPageState extends State<DemoPage>
    with TickerProviderStateMixin {
/.../
    Animation<double> _backDropFilterAnimation;
    AnimationController _backDropFilterController;
复制代码

在下面的函数中,我们首先初始化控制器并设置动画的持续时间。

_backDropFilterController = AnimationController(
    duration: Duration(milliseconds: 300),
    vsync: this,
);
复制代码

这时候我们就开始思考扩散大小的问题:以底部为中心,圆的半径逐渐增大,当半径达到时,能完全覆盖可见范围多少?

答案:√ (width² + (height * 2 + padding.top)²) / 2
平方根的一半(平方的两倍加上宽度的平方)Flutter 实现底部扩散模糊动画:跳转页面

Flutter 实现底部扩散模糊动画:跳转页面

是一个公式吗?是的,就是“勾股定理”~Flutter 实现底部扩散模糊动画:跳转页面

把勾股定理简单地用dart:math实现:

import 'dart:math' as math;

double pythagoreanTheorem(double short, double long) {
    return math.sqrt(math.pow(short, 2) + math.pow(long, 2));
}
复制代码

下面用一张图来说明问题Flutter 实现底部扩散模糊动画:跳转页面

让模糊控制完全覆盖在视图区域中,圆的漫反射半径必须大于由视图及其顶点的长度和宽度的两倍所连接的斜边的长度,而不仅仅是斜边的高度。看法。 padding.top是状态栏的高度,也应该加上高度。所以我们指定圆的结束半径,起始半径为0。现在可以写第一个Tween,用于确定圆半径的变化范围。 MediaQuery 用于获取视图的长边和短边。顺便定义一条曲线,实现曲线过渡效果。 Flutter 的 Curves 有许多内置曲线。这里我选择了Curves.easeInOut

/// 视野区域的大小(Size)
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;

/// 动画曲线
Animation _backDropFilterCurve = CurvedAnimation(
    parent: _backDropFilterController,
    curve: Curves.easeInOut,
);

/// 放大动画的设定档
Animation<double> _backDropFilterAnimation = Tween(
    begin: 0.0, end: r * 2
).animate(_backDropFilterCurve);
复制代码

这里的最终值之所以是半径的两倍,是因为圆是根据圆的外部正方形的大小来绘制的,所以这里的大小必须设置为半径的两倍才能得到真正的半径效果。

动画配置文件已完成。如果你想让动画移动,你需要将动画执行值绑定到一个变量并启动动画。因此,我们为此动画添加一个侦听器并运行 setState 来更新大小并启动​​动画。

/// 保存半径的变量
double _backdropFilterSize = 0.0;

/// 监听动画执行
_backDropFilterAnimation.addListener(() {
    setState(() {
        _backdropFilterSize = _backDropFilterAnimation.value;
    });
});

/// 正向执行动画
_backDropFilterController.forward();
复制代码

此时缩放动画就设置好了。接下来,我们将创建一个布局并将其与动画相关联。

高斯 模糊布局

我们在设置动画时就已经知道,圆圈的最终大小远远超过了视图的可见大小。要在 Flutter 中实现这样的相对或绝对布局,我们需要使用Stack。此时需要注意的是,项目Stack的overflow属性(overflow)需要设置为显示,否则圆只能扩展到最大显示宽度。

Stack(
    overflow: Overflow.visible,
    children: <Widget>[],
);
复制代码

让我们首先考虑高斯模糊区域的大小。已知圆的半径就是对角线的长度,那么由它决定的面积应该有多大呢?

再次拉动图像以查看漫反射圆相对于视图的位置:Flutter 实现底部扩散模糊动画:跳转页面

Positioned 使用绝对布局,其参考系统是视图区域。然后我们可以轻松确定顶部和水平溢出并用它来计算大小。

final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;

/// 顶部溢出大小
final double topOverflow = r - s.height;
/// 横向溢出大小
final double horizontalOverflow = r - s.width;

return Stack(
    overflow: Overflow.visible,
    children: <Widget>[
        Positioned(
            left: - horizontalOverflow,
            right: - horizontalOverflow,
            top: - topOverflow,
            bottom: - r,
/.../
复制代码

此调整范围代表圆扩大到最大半径时外方正方形的大小。

在 Flutter 中实现 高斯 模糊非常简单。只需使用BackdropFilter。一般来说,你需要用ClipRect来包裹它,以解决模糊区域的问题,而我们的需求是一个圆形,所以这里应该使用ClipRRect

import 'dart:ui' as ui;

Stack(
    overflow: Overflow.visible,
    children: <Widget>[
        Positioned(
            left: - horizontalOverflow,
            right: - horizontalOverflow,
            top: - topOverflow,
            bottom: - r,
            child: SizedBox(
                /// 高宽与变量绑定
                width: _backdropFilterSize,
                height: _backdropFilterSize,
                /// 使用圆角ClipRRect达到圆形效果
                child: ClipRRect(
                    /// 圆角的大小,使用最大值则所有时候都为圆形
                    borderRadius: BorderRadius.circular(r * 2),
                    child: BackdropFilter(
                        /// XY用于设定模糊程度
                        filter: ui.ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
                        /// 使用空格占位,否则模糊背景不显示
                        child: Text(" "),
                    ),
                ),
            ),
        ),
    ],
);
复制代码

将 高斯 的模糊控件插入到布局中,圆圈放置就完成了。

设置内容可以放置的区域

实现背景模糊后,下一步就是将内容放置在布局中适当大小的区域中。

我们圆的上半部分在可见区域中,因此我们在背景上使用对齐,使用溢出大小和可见区域的已知大小来确定内容的位置。

Stack(
    overflow: Overflow.visible,
    children: <Widget>[
        Positioned(...),
        Align(
            /// 区域相对顶部居中对齐,在可视区域附近
            alignment: Alignment.topCenter,
            child: Container(
                /// 推出顶部溢出部分,使得区域顶部对齐视图顶部
                margin: EdgeInsets.only(top: topOverflow),
                /// 将可视区域大小设定为控件大小
                width: s.width,
                height: s.height,
                /// 设置constraint,防止子控件发生意料之外的溢出
                constraints: BoxConstraints(
                    maxWidth: s.width,
                    maxHeight: s.height,
                ),
                child: child ?? SizedBox(),
            ),
        );
    ],
);
复制代码

此时我们可以轻松地将内容放入模糊区域,并在不需要时设置布局。

动画部分整体实现完成。我们将动画部分封装起来,添加到第一个完成build 中执行。

import 'package:flutter/scheduler.dart';

class _AddingButtonPageState extends State<AddingButtonPage> with TickerProviderStateMixin {
    @override
    void initState() {
        /// 使用scheduler,将动画加入到build后进行
        SchedulerBinding.instance.addPostFrameCallback((_) => backDropFilterAnimate(context));
        super.initState();
    }
    
    
    void backDropFilterAnimate(BuildContext context) async {
        final Size s = MediaQuery.of(context).size;

        _backDropFilterController = AnimationController(
            duration: Duration(milliseconds: _animateDuration),
            vsync: this,
        );
        Animation _backDropFilterCurve = CurvedAnimation(
            parent: _backDropFilterController,
            curve: Curves.easeInOut,
        );
        _backDropFilterAnimation = Tween(
            begin: 0.0,
            end: pythagoreanTheorem(s.width, s.height) * 2,
        ).animate(_backDropFilterCurve)
            ..addListener(() {
                setState(() {
                    _backdropFilterSize = _backDropFilterAnimation.value;
                });
            });
        _backDropFilterController.forward();
    }
    
/.../
复制代码

至此,底部带有漫反射模糊动画的页面跳转动画就轻松完成啦~

结论

基于几个月的潜水经验,大多数人都认为用Flutter制作动画很难因为他们看不到我理解各种属性和操作Animations,甚至连文档都看不懂,难以理解。不过,一旦真正写出来,动画部分的代码量就很少,而且很容易理解其含义。

作者:AlexV525
链接:https://juejin.im/post/5d5d1a136fb9a06ada54b66d.来源:掘金商业转载请联系作者授权。非商业转载请注明出处。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门