概要
- 自定义下拉dropdown
- 适用于tips、dropdown
- 自定义toast
一、实现思路(1)
? ? ? ? 获取父元素的大小及位置,借助Stack定位,实现自定义浮层。为了方便获取大小及编程方便,我们创建一个Dropdown组件,用于处理获取父节点信息逻辑。
import 'package:flutter/material.dart';
import 'show.dropdrown.dart';
class Dropdown extends StatefulWidget {
final Widget child;
final Widget? dropdown;
final double? dropdownWidth;
final double? dropdownHeight;
final Function? open;
final Function? close;
/// * 根据元素位置定位
/// * dropdownWidth 下拉内容宽度
/// * dropdownHeight 下拉内容高度
/// * open 展开触发
/// * close 收起触发
const Dropdown({
Key? key,
required this.child,
this.dropdown,
this.dropdownWidth,
this.dropdownHeight,
this.open,
this.close
}) : super(key: key);
@override
State<Dropdown> createState() => _DropdownState();
}
class _DropdownState extends State<Dropdown> {
void _onAfterRendering() {
RenderObject? renderObject = context.findRenderObject();
Size size = renderObject!.paintBounds.size;
var vector3 = renderObject.getTransformTo(null).getTranslation();
FocusScope.of(context).requestFocus(FocusNode());
showChooseDialog(
context:context,
size:size,
vector3:vector3,
child: widget.dropdown??const SizedBox(),
width:widget.dropdownWidth,
height: widget.dropdownHeight,
close:widget.close
);
}
@override
Widget build(BuildContext context) {
return InkWell(
child:widget.child,
onTap: () {
_onAfterRendering();
if(widget.open!=null){
widget.open!();
}
},
);
}
}
创建showChooseDialog函数,用于生成和处理浮层定位
import 'package:flutter/material.dart';
import 'Triangle.dart';
showChooseDialog({
required BuildContext context,
required Size size,
required var vector3,
required Widget child,
double? width,
double? height,
Function? close
}) {
final double wHeight =height??200;
final double wWidth = width??size.width;
final double dx = vector3[0];
final double dy = vector3[1];
final double viewWidth = MediaQuery.of(context).size.width;
final double viewHeight = MediaQuery.of(context).size.height;
//X轴定位
late double positionLeft;
late double positionLeftIcon;
//Y轴定位
late double positionTop;
late double positionTopIcon;
//方向标识
late CustomClipper<Path> icon;
late double barHeight =MediaQuery.of(context).padding.top;
//计算处理X轴定位
if(dx+wWidth<=viewWidth){
//向左未超出视窗
positionLeft =dx;
positionLeftIcon =dx+10;
}else{
//向左超出视窗
positionLeft=dx-(wWidth-size.width);
positionLeftIcon=dx+size.width-40;
}
//计算处理Y轴定位
if(dy+wHeight+10-barHeight<=viewHeight){
//向下未超出视窗
positionTop =dy +size.height+ 10-barHeight;
positionTopIcon =dy + size.height-barHeight;
icon=Triangle(dir: 1);
}else{
//向下超出视窗
positionTop=dy-wHeight-10-barHeight;
positionTopIcon=dy-10-barHeight;
icon=Triangle(dir: 0);
}
showDialog(
context: context,
barrierColor:Colors.transparent,
builder: (BuildContext context) {
return Material(
color: Colors.transparent,
elevation: 0,
child: Container(
width: wWidth,
height: double.infinity,
child: Stack(
children: <Widget>[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
),
Positioned(
left:positionLeft,
top: positionTop,
width: wWidth,
height: wHeight,
child: GestureDetector(
child:Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
boxShadow: [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.1),
offset: Offset(0.0, 1.0), //阴影y轴偏移量
blurRadius: 16, //阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
]
),
child: child
),
onTap:(){
// Navigator.of(context).pop();
}
)
),
Positioned(
left:positionLeftIcon,
top: positionTopIcon,
child: ClipPath(
clipper: icon,
child: Container(
width: 20.0,
height: 10.0,
child: null,
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.1),
offset: Offset(0.0, 1.0), //阴影y轴偏移量
blurRadius: 16, //阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
]
),
),
),
),
],
),
),
);
},
).then((value) => {
if(close!=null){
close()
}
});
}
Triangle函数这是用于画三角符号
import 'package:flutter/material.dart';
class Triangle extends CustomClipper<Path> {
double dir;
Triangle({required this.dir});
@override
Path getClip(Size size) {
var path = Path();
if (dir ==1) {
path.moveTo(size.width / 2, 0);
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.close();
} else {
path.moveTo(0, 0);
path.lineTo(size.width , 0);
path.lineTo(size.width/2, size.height);
path.close();
}
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return false;
}
}
实际调用
class TestToast extends StatefulWidget {
const TestToast({Key? key}) : super(key: key);
static const routeName = '/testtoast';
@override
State<TestToast> createState() => _TestToastState();
}
class _TestToastState extends State<TestToast> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Container(
height: 100,
width: 100,
color:Colors.red,
alignment: Alignment.center,
child: const Dropdown(
child: Text('父节点')
),
),
),
);
}
}
效果
二、实现思路(2)
? ? ? ? 借助CompositedTransformFollower及CompositedTransformTarget组件,制作联合组件。结合OverlayEntry动态插入节点。相比方案一,这个方案更加的优越,其下拉不会影响页面的其他插座。
新建类PickerDownDialog.dart用于下拉框生成。
import 'dart:ui';
import 'package:flutter/material.dart';
class PickerDownDialog{
BuildContext context;
final Widget? child;
final Widget Function(BuildContext context)? builder;
double? width;
double? height;
final LayerLink layerLink;
PickerDownDialog({
required this.context,
this.builder,
this.child,
this.height,
this.width,
required this.layerLink
});
late OverlayEntry? _overlayEntry=null;
late bool hasOpenedOverlay = false;
void openOverlay() {
if (!(_overlayEntry != null)) {
RenderBox renderBox = context.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
final double wHeight =height??240;
final double wWidth = width??size.width;
final double dx = offset.dx;
final double dy = offset.dy;
final double viewWidth = MediaQuery.of(context).size.width;
final double viewHeight = MediaQuery.of(context).size.height;
//X轴定位
late double positionLeft;
//Y轴定位
late double positionTop;
//计算处理X轴定位
if(dx+wWidth<=viewWidth){
//向左未超出视窗
positionLeft =dx;
}else{
//向左超出视窗
positionLeft=dx-(wWidth-size.width);
}
//计算处理Y轴定位
if(dy+wHeight+5<=viewHeight){
//向下未超出视窗
positionTop =dy +size.height+ 5;
}else{
//向下超出视窗
positionTop=dy-wHeight;
}
_overlayEntry ??= OverlayEntry(
builder: (context) => Positioned(
left:positionLeft,
top: positionTop,
width: wWidth,
height: wHeight,
child:CompositedTransformFollower(
link: layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 5.0),
child:Material(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
boxShadow: [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.1),
offset: Offset(0.0, 1.0), //阴影y轴偏移量
blurRadius: 16, //阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
]
),
child: child??builder!(context),
),
)
)
)
);
}
if (!hasOpenedOverlay) {
Overlay.of(context)!.insert(_overlayEntry!);
hasOpenedOverlay = true;
}
}
void closeOverlay() {
if (hasOpenedOverlay) {
_overlayEntry!.remove();
hasOpenedOverlay = false;
}
}
}
在组件关联,CompositedTransformTarget,通过pickerDownDialog.openOverlay();pickerDownDialog.openOverlay();实现下拉框显示隐藏控制。这样实现的效果,在切换页面tab时会不影响。
?
三、自定义Toast
? ? ? ? 通过使用OverlayEntry插入、Positioned层叠定位来实现。这个提示层不会影响用户对其他页面的操作,外层不会有笼罩层。纯展示提示。
// ignore_for_file: unnecessary_new
import 'package:flutter/material.dart';
class Toast {
static void show({required BuildContext context, required String message}) {
//创建一个OverlayEntry对象
OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
//外层使用Positioned进行定位,控制在Overlay中的位置
return new Positioned(
top: MediaQuery.of(context).size.height * 0.9,
child: new Material(
color: Colors.transparent,
child: new Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
child: new Center(
child: new Card(
child: new Padding(
padding: const EdgeInsets.all(8),
child: new Text(message),
),
color: Colors.transparent,
shadowColor: Colors.transparent,
elevation: 0,
),
),
),
));
});
//往Overlay中插入插入OverlayEntry
Overlay.of(context)!.insert(overlayEntry);
//两秒后,移除Toast
new Future.delayed(const Duration(seconds: 2)).then((value) {
overlayEntry.remove();
});
}
}
实现效果
|