Flutter 模仿知乎加号按键 添加半透明路由页面

效果

本文使用flutter模仿知乎的加号按键,实现如图效果:

1p2.gif

分析思路

仔细分析,上述效果包含了以下几个关键点:点击按键在原有页面上添加一个新页面;新页面半透明高斯模糊,露出上层页面;弹出时有上滑动画;点击新页面的空白处返回。

简单来说,添加新页面是通过在Stack上放置新页面实现的(直接Navigator.of(context).push的新页面无法露出下层的效果);
动画用Transform.translate实现;
高斯模糊用BackDropFilter实现;
点击新页面空白处则是在全局和非空白处设GestureDetector(利用冒泡特性),在合适时机调用返回的回调函数。

具体实现

点击后在栈上添加页面

父页面
Stack的build函数,这里_mainBackPage()是背后默认显示的页面,_addPageStack是用来存放半透明页面的栈(其默认为空)。

![1p.gif](https://upload-images.jianshu.io/upload_images/21366722-ab0267e5171d5b26.gif?imageMogr2/auto-orient/strip)

  //...
  List<Widget> _addPageStack = [];
  //...
  @override
  void initState() {
    //...
    _addPageStack = [];
  }
@override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[_mainBackPage()]..addAll(_addPageStack),
      alignment: Alignment.center,
    );
  }

以下为BottomNavigationBar项的点击事件,点击加号的时候给_addPageStack栈添加AddPage页面(由于子页面要有点击空白返回的回调函数,所以需要传入callback)。

bottomNavigationBar: 
          child: BottomNavigationBar(
            onTap: (int index) {
              //...
              // 如果点击了加号的Item,触发以下事件
                setState(() {
                  _addPageStack.add(AddPage(callback: _onTapBackground));
              //...

页面半透明 高斯模糊

在子页面中使用如下组件包裹即可。

child: BackdropFilter(
              filter: prefix0.ImageFilter.blur(sigmaX: 6, sigmaY: 6),

弹出的动画

弹出动画原理为在子页面被push到_addStackPage中时,子页面在initState中调用初始动画。
这里使用了Tween对于高度Offset(动画部分用Transform.translate包裹并设置了offset属性)做了补间,形成动画。
此外为了上滑有加速度,先让animation为CurvedAnimation,选择easeIn曲线来进入。

@override
  void initState() {
    super.initState();
    animationController =
        new AnimationController(vsync: this, duration: Duration(milliseconds: 200));
    animation = CurvedAnimation(parent: animationController,curve: Curves.easeIn);
    animation = new Tween(begin: 200.0,end:0.0).animate(animation)
      ..addListener(() {
        setState(() {});
      });
    animationController.forward();
  }
//...
//build组件中
body: Transform.translate(
          offset: Offset(0, animation.value),

点击空白处返回

还记得父页面中传入的callback函数_onTapBackground吗?子页面点击空白返回就是在GestureDetector监听到触摸空白时调用此函数。
父页面传入:

bottomNavigationBar: 
          child: BottomNavigationBar(
            onTap: (int index) {
              //...
              // 如果点击了加号的Item,触发以下事件
                setState(() {
                  _addPageStack.add(AddPage(callback: _onTapBackground));
              //...

父页面传入的函数定义:

  _onTapBackground() {
    setState(() {
      _addPageStack.removeLast();
    });
  }

子页面的实现可以直接看下方的完整子页面,其逻辑就是整个子页面包裹了一个GestureDetector(其onTap事件就是父页面传入的callback),非空白区域(Container卡片)包裹了一个GestureDetector(其onTap事件不做事情)。
由于GestureDetector的冒泡机制,在子容器监听触摸事件时,拦截,不向父传递;故内层的GestureDetector效果即为屏蔽非空白区域。
完整代码可看下方。

完整的子页面

import 'dart:ui' as prefix0;

import 'package:flutter/material.dart';

class AddPage extends StatefulWidget {
  final callback;

  AddPage({Key key, this.callback});

  @override
  _AddPageState createState() => _AddPageState();
}

class _AddPageState extends State<AddPage> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController animationController;

  @override
  void initState() {
    super.initState();
    animationController =
        new AnimationController(vsync: this, duration: Duration(milliseconds: 200));
    animation = CurvedAnimation(parent: animationController,curve: Curves.easeIn);
    animation = new Tween(begin: 200.0,end:0.0).animate(animation)
      ..addListener(() {
        setState(() {});
      });
    animationController.forward();
  }


  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        widget.callback();
      },
      child: Scaffold(
        backgroundColor: Colors.black45,
        body: Transform.translate(
          offset: Offset(0, animation.value),
          child: Container(
            alignment: Alignment.bottomCenter,
            child: BackdropFilter(
              filter: prefix0.ImageFilter.blur(sigmaX: 6, sigmaY: 6),
              child: GestureDetector(
                onTap: () {},
                child: Container(     // 内层的卡片
                  height: 300,
                  width: 200,
                  decoration: BoxDecoration(
                      border: Border.all(color: Colors.red, width: 10),
                      borderRadius: BorderRadius.circular(25)),
                  alignment: Alignment.center,
                  child: Column(
                    children: <Widget>[
                      Icon(Icons.person),
                      Container(
                        height: 25,
                        width: 150,
                        color: Colors.blue,
                      )
                    ],
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,394评论 0 2
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,356评论 0 3
  • 前端开发面试题 面试题目: 根据你的等级和职位的变化,入门级到专家级,广度和深度都会有所增加。 题目类型: 理论知...
    怡宝丶阅读 2,613评论 0 7
  • 已是立秋的第六天,天气依然闷热,窗外的知了鸣声清脆。正是葡萄成熟的好时节,您活着的时候最爱吃葡萄。这是您去世的第七...
    一言二丫阅读 382评论 0 1
  • 一个电话又是长长的想念 女儿刚刚给我打电话,她说不想在学校睡。你想在那睡你让妈妈怎么办呢?让你在家上学吗?学费都已...
    馨之芬芳阅读 137评论 3 2