Flutter 实现底部扩散模糊动画(二)页面交互

相关文章

前言

  在上一期,咱们已经完成了点开动画的编写和执行,若是有仔细看完的小伙伴会发现,其中的动画效果不止扩散这么简单,本篇就来继续研究其他的动画交互。git

简介

  做为一个炫(pin)酷(ru)的页面,页面中的交互也很是的重要。在本篇,我将进一步说明页面内各个位置的交互细节,从而带着各位作一个不将就的强迫症~github

  效果图: 数组

  完整demo及组件已上传至项目,走过路过留个star~bash

交互要素

  页面中的交互主要包含三个触发位置:markdown

  • 点击空白的模糊处,页面会执行退出和退出动画;
  • 点击页面上的返回或关闭按钮,页面会执行退出和退出动画;
  • 元素渐显并带有其余效果。

  接下来将逐点说明如何实现。app

实现过程

拦截返回操做

  咱们知道在Flutter中,页面要返回时,会执行Navigator.maybePop的方法,使页面返回。为了拦截路由pop,Flutter提供了WillPopScope来拦截返回行为,咱们只须要注册onWillPop方法,就能够在pop前执行代码。async

bool _popping = false;

Future<bool> willPop() async {
    /// 等待返回动画的执行
    await backDropFilterAnimate(context, false);
    /// 判断_popping从而避免重复触发pop
    if (!_popping) {
        _popping = true;
        await Future.delayed(Duration(milliseconds: _animateDuration), () {
            Navigator.of(context).pop();
        });
    }
    return null;
}

@override
Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.transparent,
        body: WillPopScope(
            /// 绑定willPop方法
            onWIllPop: willPop,
            child: wrapper(
                context,
                child: widget.child,
            ),
        ),
    );
}
复制代码

  如此咱们就轻松愉快地拦截了路由~ide

退出动画

  思考退出动画和跳转动画的关系,咱们立马就能够想到,跳转和退出的动画是相反的,也就是说,逆向执行跳转的动画,就能获得一个退出动画。布局

  这时咱们来回顾一下上一期的跳转动画:post

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();
}
    
复制代码

  要想以相反的方向执行动画,咱们加入一个参数bool forward

void backDropFilterAnimate(BuildContext context, bool forward)

  使用forward来控制beginend,达到执行的效果。同时对forward进行判断,若是为false尝试暂停动画

void backDropFilterAnimate(BuildContext context, bool forward) {
    /.../
    if (!forward) _backDropFilterController?.stop();
    
    _backDropFilterAnimation = Tween(
        /// 三元运算赋值
        begin: forward ? 0.0 : _backdropFilterSize,
        end: forward ? pythagoreanTheorem(s.width, s.height) * 2 : 0.0,
    ).animate(_backDropFilterCurve)
        ..addListener(() {
            setState(() {
                _backdropFilterSize = _backDropFilterAnimation.value;
            });
        });
    
    /.../
}
复制代码

  看到这里可能会有小伙伴问了,AnimateController明明提供了reverse方法用于反向,为何还要使用一个bool来控制动画执行方向呢?

  缘由在于当使用reverse时,控制器会将beginend对调来执行动画,但当咱们执行退出动画时,圆形不必定已经彻底覆盖,因此经过使用forward来判断方向,能够使未彻底覆盖的动画从中止处反向执行,不会形成闪烁的状况。

  至此,跳转和退出动画已经完美完成。

"X" & 空白处返回

  根据效果图,在页面的底部,会提供一个带有旋转动画返回按钮,点击能够返回。

  因为个人页面时点击加号触发的,因此这里我引入了bottomHeight,用来肯定加号的位置。从效果图能够看到个人底部导航栏,它的高度咱们假设是60.0,那按钮的位置如何定义呢?

final double bottomHeight = 60.0;
/.../
Widget popButton() {
    return SizedBox(
        /// 此处假设为60.0
        width: widget.bottomHeight,
        height: widget.bottomHeight,
        child: Center(
            /// 套手势监听,并设定监听行为
            child: GestureDetector(
                behavior: HitTestBehavior.opaque,
                child: Icon(
                    Icons.add,
                    color: Colors.grey
                ),
                onTap: willPop,
            ),
        ),
    );
}
复制代码

  将它放入布局中:

Stack(
    /.../
    children: <Widget>[
        Positioned(
            /// 将按钮控件固定在视图底部中央
            left: 0.0,
            right: 0.0,
            bottom: 0.0,
            child: popButton(),
        ),
    ],
)
复制代码

  按钮定位完成,这时咱们开始设计动画。按钮一共须要两组动画,一组是旋转,一组是淡入淡出。

/// 初始化按钮旋转的角度
final double bottomButtonRotateDegree = 45.0;

/// 旋转动画相关
Animation<double> _popButtonAnimation;
AnimationController _popButtonController;
/// 淡入淡出相关
Animation<double> _popButtonOpacityAnimation;
AnimationController _popButtonOpacityController;

void popButtonAnimate(context, bool forward) {
    /// 与背景相同,判断正反执行
    if (!forward) {
        _popButtonController?.stop();
        _popButtonOpacityController?.stop();
    }
    /// 转换按钮实际旋转角度
    final double rotateDegree =
        widget.bottomButtonRotateDegree * (math.pi / 180);
        
    /// 
    _popButtonOpacityController = _popButtonController = AnimationController(
        duration: Duration(milliseconds: _animateDuration),
        vsync: this,
    );
    Animation _popButtonCurve = CurvedAnimation(
        parent: _popButtonController,
        curve: Curves.easeInOut,
    );
    _popButtonAnimation = Tween(
        begin: forward ? 0.0 : _popButtonRotateAngle,
        end: forward ? rotateDegree : 0.0,
    ).animate(_popButtonCurve)
        ..addListener(() {
            setState(() {
                _popButtonRotateAngle = _popButtonAnimation.value;
            });
        });
    /// 设定透明度最小值为0.01,防止背景显示错误
    _popButtonOpacityAnimation = Tween(
        begin: forward ? 0.01 : _popButtonOpacity,
        end: forward ? 1.0 : 0.01,
    ).animate(_popButtonCurve)
        ..addListener(() {
            setState(() {
                _popButtonOpacity = _popButtonOpacityAnimation.value;
            });
        });
    _popButtonController.forward();
    _popButtonOpacityController.forward();
}
复制代码

  按钮动画构建完成,咱们将它放到背景动画中一块儿执行:

Future backDropFilterAnimate(BuildContext context, bool forward) async {
    /.../
    /// 使用相同的forward控制方向
    popButtonAnimate(context, forward);
    /.../
}
复制代码

  至此,按钮的动画会跟着背景一块儿联动了,十分完美~

  但,别着急结束,咱们还有内容的动画定制没有完成,若是不须要如效果图通常的元素动画,能够出门右转~

操做项动画

  从效果图咱们能够看到,两个操做项是依次淡入出现,而且带有必定的垂直位移。这时问题出现了:个人操做项数量不肯定,难道每个操做项我都要专门写一个动画吗?

  答案是:对了一半。为何这么说?咱们确实须要写操做项的动画,但咱们不须要重复地去写每个操做项,只须要经过封装操做项的内容,将动画全部相关内容也组成数个List,问题就简单了不少。

  以效果图为例,我有两个操做项,先进行声明。

List<String> itemTitles = ["动态", "扫一扫"];
List<String> itemIcons = ["subscriptedAccount", "scan"];
List<Color> itemColors = [Colors.orange, Colors.teal];
List<Function> itemOnTap = [...];
复制代码

  将操做项全部的信息存储在四个数组中。接下来咱们建立两组动画共8个数组的相关变量。

/// 操做项垂直偏移量
List<double> _itemOffset;
/// 操做项偏移动画
List<Animation<double>> _itemAnimations;
/// 操做项偏移动画曲线
List<CurvedAnimation> _itemCurveAnimations;
/// 操做项偏移动画控制器
List<AnimationController> _itemAnimateControllers;
/// 操做项透明度
List<double> _itemOpacity;
/// 操做项透明度动画
List<Animation<double>> _itemOpacityAnimations;
/// 操做项透明度动画曲线
List<CurvedAnimation> _itemOpacityCurveAnimations;
/// 操做项透明度动画控制器
List<AnimationController> _itemOpacityAnimateControllers;
复制代码

  那么,该怎么初始化动画呢?

void initItemsAnimation() {
    /// 根据操做项内容,初始化动画相关变量
    _itemOffset = <double>[for (int i=0; i<itemTitles.length; i++) 0.0];
    _itemAnimations = List<Animation<double>>(itemTitles.length);
    _itemCurveAnimations = List<CurvedAnimation>(itemTitles.length);
    _itemAnimateControllers = List<AnimationController>(itemTitles.length);
    _itemOpacity = <double>[for (int i=0; i<itemTitles.length; i++) 0.01];
    _itemOpacityAnimations = List<Animation<double>>(itemTitles.length);
    _itemOpacityCurveAnimations = List<CurvedAnimation>(itemTitles.length);    _itemOpacityAnimateControllers = List<AnimationController>(itemTitles.length);
    
    /// 遍历操做性,初始化每个动画内容
    for (int i = 0; i < itemTitles.length; i++) {
        /// 垂直偏移动画的设定
        _itemAnimateControllers[i] = AnimationController(
            duration: Duration(milliseconds: _animateDuration),
            vsync: this,
        );
        _itemCurveAnimations[i] = CurvedAnimation(
            parent: _itemAnimateControllers[i],
            curve: Curves.ease,
        );
        /// 垂直偏移量设置为20
        _itemAnimations[i] = Tween(
            begin: -20.0,
            end: 0.0,
        ).animate(_itemCurveAnimations[i])                ..addListener(() {
                setState(() {
                    _itemOffset[i] = _itemAnimations[i].value;
                });
            });
        
        /// 透明度动画的设定
        _itemOpacityAnimateControllers[i] = AnimationController(
            duration: Duration(milliseconds: _animateDuration),
            vsync: this,
        );
        _itemOpacityCurveAnimations[i] = CurvedAnimation(
            parent: _itemOpacityAnimateControllers[i],
            curve: Curves.linear,
        );
        _itemOpacityAnimations[i] = Tween(
            begin: 0.01,
            end: 1.0,
        ).animate(_itemOpacityCurveAnimations[i])
            ..addListener(() {
                setState(() {
                    _itemOpacity[i] = _itemOpacityAnimations[i].value;
                });
            });
    }
}

/// 操做项动画的执行
void itemsAnimate(bool forward) {
    for (int i = 0; i < _itemAnimateControllers.length; i++) {
        /// 每一个操做项依次增长延时,造成连续效果
        Future.delayed(Duration(milliseconds: 50 * i), () {
            if (forward) {
                _itemAnimateControllers[i]?.forward();
                _itemOpacityAnimateControllers[i]?.forward();
            } else {
                _itemAnimateControllers[i]?.reverse();
                _itemOpacityAnimateControllers[i]?.reverse();
            }
        });
    }
}
复制代码

  建立操做项的widget,将动画值进行绑定:

Widget item(BuildContext context, int index) {
    return Stack(
        overflow: Overflow.visible,
        children: <Widget>[
            Positioned(
                left: 0.0, right: 0.0,
                /// 绑定垂直偏移
                top: _itemOffset[index],
                child: Opacity(
                    /// 绑定透明度
                    opacity: _itemOpacity[index],
                    child: ...
                ),
            ),
        ],
    );
}
复制代码

  最后将动画初始化放进initState,动画执行添加至跳转动画。

@override
void initState() {
    initItemsAnimation();
    /.../
}

Future backDropFilterAnimate(BuildContext context, bool forward) async {
    /.../
    if (forward) {
        /// 以跳转动画二分之一的延时执行,效果更佳
        Future.delayed(
            Duration(milliseconds: _animateDuration ~/ 2),
            () { itemsAnimate(true); },
        );
    } else {
        itemsAnimate(false);
    }
}
复制代码

  一切就绪,保存就能够看到精美的动画效果了~

结语

  这个动画我的耗时大约2小时,在思路很是清晰的状况下,将动画效果实现不是一件难事,这样的动画其实相对不难,接下来可能会有内容揭开、位置自定义等花式的需求,让咱们拭目以待~

  最后欢迎加入Flutter Candies,一块儿生产可爱的Flutter小糖果 (QQ群:181398081)

相关文章
相关标签/搜索