效果
本文使用flutter模仿知乎的加号按键,实现如图效果:
分析思路
仔细分析,上述效果包含了以下几个关键点:点击按键在原有页面上添加一个新页面;新页面半透明高斯模糊,露出上层页面;弹出时有上滑动画;点击新页面的空白处返回。
简单来说,添加新页面是通过在Stack上放置新页面实现的(直接Navigator.of(context).push的新页面无法露出下层的效果);
动画用Transform.translate实现;
高斯模糊用BackDropFilter实现;
点击新页面空白处则是在全局和非空白处设GestureDetector(利用冒泡特性),在合适时机调用返回的回调函数。
具体实现
点击后在栈上添加页面
父页面
Stack的build函数,这里_mainBackPage()是背后默认显示的页面,_addPageStack是用来存放半透明页面的栈(其默认为空)。

//...
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();
}
}