flutter 实现弹出窗 点击下拉栏 微信右上角弹出窗
程序员文章站
2022-06-18 18:31:21
先看效果实现 需求分析 这个是使用 PopupRoute这个路由类进行实现 大概原理就是利用PopupRpute这个类进行改造,然后自定义一个页面,页面内镶嵌一个动画类,用来实现缩放动画 大概分为三部分,PopupRoute改造,弹出页面设置,动画类设置。 为什么选择PopupRoute? 可以镶嵌 ......
先看效果实现
需求分析
这个是使用 popuproute这个路由类进行实现
大概原理就是利用popuprpute这个类进行改造,然后自定义一个页面,页面内镶嵌一个动画类,用来实现缩放动画
大概分为三部分,popuproute改造,弹出页面设置,动画类设置。
为什么选择popuproute?
可以镶嵌在flutter本身的路由管理之中
也就是逻辑操作都是正常的页面管理,可以手动管理,也可以用路由返回直接关掉,不会影响原有页面和布局
第一步,改造popuproute类
import 'package:flutter/material.dart';
class popup extends popuproute {
final duration _duration = duration(milliseconds: 300);
widget child;
popup({@required this.child});
@override
color get barriercolor => null;
@override
bool get barrierdismissible => true;
@override
string get barrierlabel => null;
@override
widget buildpage(buildcontext context, animation<double> animation,
animation<double> secondaryanimation) {
return child;
}
@override
duration get transitionduration => _duration;
}
第二步,新建一个弹窗页面
页面分两部分
一个是页面的背景,一个是页面的内容
注意,弹窗动画的代码在下方
class model extends statefulwidget {
final double left; //距离左边位置 弹窗的x轴定位
final double top; //距离上面位置 弹窗的y轴定位
final bool otherclose; //点击背景关闭页面
final widget child; //传入弹窗的样式
final function fun; // 把关闭的函数返回给父组件 参考vue的$emit
final offset offset; // 弹窗动画的起点
model({
@required this.child,
this.left = 0,
this.top = 0,
this.otherclose = false,
this.fun,
this.offset,
});
@override
_modelstate createstate() => _modelstate();
}
class _modelstate extends state<model> {
animationcontroller animatecontroller;
@override
widget build(buildcontext context) {
return material(
color: colors.transparent,
child: stack(
children: <widget>[
positioned(
child: gesturedetector(
child: container(
width: mediaquery.of(context).size.width,
height: mediaquery.of(context).size.height,
color: colors.transparent,
),
ontap: () async {
if (widget.otherclose) {
} else {
closemodel();
}
},
),
),
positioned(
/// 这个是弹窗动画 在下方,我把他分离 防止太长
child: zoominoffset(
duration: duration(milliseconds: 180),
offset: widget.offset,
controller: (controller) {
animatecontroller = controller;
widget.fun(closemodel);
},
child: widget.child,
),
left: widget.left,
top: widget.top,
),
],
),
);
}
///关闭页面动画
future closemodel() async {
await animatecontroller.reverse();
navigator.pop(context);
}
}
动画代码
我是直接复制 animate_do: ^2.0.0 这个版本的zoomin的动画类
这个插件本身就是依赖flutter 自带的动画来完成的,很简洁,使用很方便,
不过默认构造的时候没有动画的启动方向,默认是最中心。
但是可以添加个参数,我把源码复制出来自己改造了一下。
这个类在构造的时候有个 controller 参数,类型的函数,带一个animationcontroller的参数
把控制器通过函数传递出去到model类,可以在model类里面进行控制动画开启和关闭
后续我在model类里面把动画关闭和返回退出popuproute层封装成一个函数 传递到model里面的fun参数里面返回出去
可以在最外部进行组件通信,进而控制这些子组件
import 'package:flutter/material.dart';
class zoominoffset extends statefulwidget {
final key key;
final widget child;
final duration duration;
final duration delay;
///把控制器通过函数传递出去,可以在父组件进行控制
final function(animationcontroller) controller;
final bool manualtrigger;
final bool animate;
final double from;
///这是我自己写的 起点
final offset offset;
zoominoffset(
{this.key,
this.child,
this.duration = const duration(milliseconds: 500),
this.delay = const duration(milliseconds: 0),
this.controller,
this.manualtrigger = false,
this.animate = true,
this.offset,
this.from = 1.0})
: super(key: key) {
if (manualtrigger == true && controller == null) {
throw fluttererror('if you want to use manualtrigger:true, \n\n'
'then you must provide the controller property, that is a callback like:\n\n'
' ( controller: animationcontroller) => yourcontroller = controller \n\n');
}
}
@override
_zoominstate createstate() => _zoominstate();
}
/// state class, where the magic happens
class _zoominstate extends state<zoominoffset>
with singletickerproviderstatemixin {
animationcontroller controller;
bool disposed = false;
animation<double> fade;
animation<double> opacity;
@override
void dispose() async {
disposed = true;
controller.dispose();
super.dispose();
}
@override
void initstate() {
super.initstate();
controller = animationcontroller(duration: widget.duration, vsync: this);
fade = tween(begin: 0.0, end: widget.from)
.animate(curvedanimation(curve: curves.easeout, parent: controller));
opacity = tween<double>(begin: 0.0, end: 1)
.animate(curvedanimation(parent: controller, curve: interval(0, 0.65)));
if (!widget.manualtrigger && widget.animate) {
future.delayed(widget.delay, () {
if (!disposed) {
controller?.forward();
}
});
}
if (widget.controller is function) {
widget.controller(controller);
}
}
@override
widget build(buildcontext context) {
if (widget.animate && widget.delay.inmilliseconds == 0) {
controller?.forward();
}
return animatedbuilder(
animation: fade,
builder: (buildcontext context, widget child) {
/// 这个transform有origin的可选构造参数,我们可以手动添加
return transform.scale(
origin: widget.offset,
scale: fade.value,
child: opacity(
opacity: opacity.value,
child: widget.child,
),
);
},
);
}
}
最后页面调用
我用stack类进行堆叠组件,堆叠出上面箭头
其实可以抽成一个方向设置不过太麻烦了我没写,毕竟能用就行
import 'package:flutter/material.dart';
import 'package:one/widget/model.dart';
import 'package:one/widget/popup.dart';
void main() {
runapp(myapp());
}
class myapp extends statefulwidget {
@override
_myappstate createstate() => _myappstate();
}
class _myappstate extends state<myapp> {
///给获取详细信息的widget设置一个key
globalkey iconkey = globalkey();
///获取位置,给后续弹窗设置位置
offset iconoffset;
///获取size 后续计算弹出位置
size iconsize;
///接受弹窗类构造成功传递来的关闭参数
function closemodel;
@override
widget build(buildcontext context) {
///等待widget初始化完成
widgetsbinding.instance.addpostframecallback((duration) {
///通过key获取到widget的位置
renderbox box = iconkey.currentcontext.findrenderobject();
///获取widget的高宽
iconsize = box.size;
///获取位置
iconoffset = box.localtoglobal(offset.zero);
});
return materialapp(
home: builder(
builder: (context) => scaffold(
appbar: appbar(
actions: [
iconbutton(
key: iconkey,
icon: icon(
icons.favorite,
color: colors.red,
),
onpressed: () {
showmodel(context);
},
),
],
),
body: column(
children: [],
),
),
),
);
}
///播放动画
void showmodel(buildcontext context) {
/// 设置传入弹窗的高宽
double _width = 130;
double _height = 230;
navigator.push(
context,
popup(
child: model(
left: iconoffset.dx - _width + iconsize.width / 1.2,
top: iconoffset.dy + iconsize.height / 1.3,
offset: offset(_width / 2, -_height / 2),
child: container(
width: _width,
height: _height,
child: buildmenu(),
),
fun: (close) {
closemodel = close;
},
),
),
);
}
///构造传入的widget
widget buildmenu() {
///构造list
list _list = [1, 2, 3, 4, 5];
return container(
height: 160,
width: 230,
child: stack(
children: [
positioned(
right: 4,
top: 17,
child: container(
width: 20,
height: 20,
transform: matrix4.rotationz(45 * 3.14 / 180),
decoration: boxdecoration(
color: color.fromrgbo(46, 53, 61, 1),
borderradius: borderradius.circular(5),
),
),
),
///菜单内容
positioned(
bottom: 0,
child: container(
padding: edgeinsets.only(
top: 20,
bottom: 20,
left: 10,
right: 10,
),
width: 130,
height: 200,
decoration: boxdecoration(
borderradius: borderradius.circular(10),
color: color.fromrgbo(46, 53, 61, 1),
),
child: column(
mainaxisalignment: mainaxisalignment.spacebetween,
children: _list
.map<widget>((e) => inkwell(
child: container(
width: double.infinity,
alignment: alignment.center,
child: text(
'这应该是选项${e.tostring()}',
style: textstyle(
color: colors.white70,
fontsize: 14,
),
),
),
ontap: () async {
print('这是点击了选项${e.tostring()}');
await future.delayed(duration(milliseconds: 500))
.then((value) => print('开始'));
await closemodel();
print('结束');
},
))
.tolist(),
),
),
),
],
),
);
}
}
然后就能实现我们的弹窗动画了,如果想要其他效果的动画,可以手动替换动画类,或者自己手写个新的
最后我自己的项目修饰效果,还有demo的代码
代码 仓库地址:https://github.com/mannaoz/one