一、需求
![](https://img.haomeiwen.com/i4163152/5d3ef79a790675e2.png)
二、思路
先把下面的悬浮窗背景和三角形自定义绘制一个widget。然后再把标签和悬浮框做一个联动(CompositedTransformFollower + CompositedTransformTarget),这样即使整个标签弹窗滑动,悬浮窗也能跟着标签跑。
三、悬浮窗带三角指示器背景自定义widget
- 先确定起点,使用path绘制下一个三角形。
- 在使用path绘制一个矩形。
- 使用path的api把两个path联合起来。
代码展示如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
///
/// @ProjectName: plugin_base
/// @Package: lib.common.global_store
/// @ClassName: base_appbar
/// @Description: 写一个指定三角大小,指定三角偏移量的,带背景颜色的widget
/// @Author: yang
/// @CreateDate: 2021/8/5 10:18
/// @UpdateDate: 2021/8/5 10:18
/// @UpdateRemark: 更新说明
/// @Version: 1.0
class ThreeSan extends StatelessWidget{
///三角形是否居中
bool isCenter;
///偏移量
final double offset;
///三角形的高
final double sanHeight;
///三角形的宽度
double sanWidth;
///三角形的顶部角度
final int sanAngle;
///间距
double? padding;
Key? key;
///子Widget
Widget child;
///矩形的圆角
double radius;
///背景颜色
Color background;
ThreeSan(this.isCenter,this.offset,this.sanWidth,this.sanHeight,this.child,this.radius,this.padding,
this.background,this.sanAngle,
{this.key}) : super(key: key);
@override
Widget build(BuildContext context) {
var _padding = padding;
return Padding(
padding: EdgeInsets.all(_padding!),
child: CustomPaint(
child: child,
painter: ThreeSanPainter(isCenter,offset,radius,sanWidth,sanHeight,sanAngle,background),
),
);
}
}
class ThreeSanPainter extends CustomPainter{
bool isCenter;
double offset;
double radius;
double sanWidth;
double sanHeight;
int sanAngle;
Color background;
ThreeSanPainter(this.isCenter,this.offset,this.radius,this.sanWidth,this.sanHeight,
this.sanAngle,this.background);
@override
void paint(Canvas canvas, Size size) {
/// 绘制矩形
var height = size.height ;
var width = size.width;
Rect rect = Rect.fromCenter(center: Offset(width/2,height/2), width: width, height: height);
Path path = Path()..addRRect(RRect.fromRectXY(rect, radius, radius));
///绘制三角
double sanJaoHeight = sanHeight;
var sanJiaoMovex = sanWidth/2;
var sanJiaoMovey = sanJaoHeight;
Path sanjiaoPath = Path()..moveTo(isCenter ? size.width/2 - sanWidth/2 : offset, 0)
..relativeLineTo(sanJiaoMovex, -sanJiaoMovey)
..relativeLineTo(sanJiaoMovex, sanJiaoMovey);
///联合两个path
path = Path.combine(PathOperation.union, sanjiaoPath, path);
canvas.drawPath(path, Paint()..color = background);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
结果展示如下:
![](https://img.haomeiwen.com/i4163152/8d4ed6917af6b44b.png)
![](https://img.haomeiwen.com/i4163152/c97118bf8ad22687.png)
三、联动标签和悬浮窗
参考之前的文章 Flutter绑定两个View,CompositedTransformFollower 与 CompositedTransformTarget
这里是因为整个标签弹窗是可以上下滑动的,所以,点击标签弹出悬浮窗需要跟着标签和标签弹窗一起动。
其中也遇到了几个问题。
1,点击标签的悬浮窗大小不受控制。后面也是通过试出来,需要使用根节点为ConstrainedBox来保证悬浮窗不至于充满屏幕。
![](https://img.haomeiwen.com/i4163152/be816bbe70ef8b32.png)
2.通过发消息来解决:点击下一个标签的时候,把上一个悬浮窗关闭。
3.监听标签底部弹窗关闭来销毁悬浮窗,销毁弹窗时候注意_overlayEntry?.remove(); 不能重复去销毁,会直接报错。
联动控件代码
///悬浮框控件 两个空间联动
class TipBox extends StatefulWidget{
Color? textColor;
String? text;
String? labelDefinition;
TipBox(this.textColor, this.text, this.labelDefinition);
@override
State<StatefulWidget> createState() {
return TipBoxState();
}
}
class TipBoxState extends State<TipBox>{
final LayerLink layerLink = LayerLink();
bool show = false;
OverlayEntry? _overlayEntry;
@override
void initState() {
eventBus.on<OverlayEntry?>().listen((event) {
debugPrint("接收到取消弹窗的消息");
if(_overlayEntry != event && show){
hideOverlay() ;
show = !show;
}
});
super.initState();
}
@override
void dispose() {
_overlayEntry?.remove();
_overlayEntry = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: ()=>_toggleOverlay(context),
child: CompositedTransformTarget(
link: layerLink,
child: backgroundText(widget.textColor,widget.text),
),
);
}
void _toggleOverlay(BuildContext context){
show ? hideOverlay() : showOverlay(context);
show = !show;
///发消息,把除了当前的弹窗都关闭掉
eventBus.fire(_overlayEntry);
}
showOverlay(BuildContext context){
_overlayEntry = _createOverlayEntry();
Overlay.of(context)!.insert(_overlayEntry!);
}
OverlayEntry _createOverlayEntry() => OverlayEntry(
builder: (BuildContext context) => CompositedTransformFollower(
link: layerLink,
followerAnchor: Alignment.topCenter,
targetAnchor: Alignment.bottomCenter,
offset: Offset(0,5),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 1.sw,maxHeight: 1.sh),
child: Align(
alignment: Alignment.topCenter,
child: Container(
child: ThreeSan(true,0,16.0,6.0,
Padding(
padding: const EdgeInsets.all(10),
child: Text(interceptString(widget.labelDefinition),
style: const TextStyle(color: Colors.white),),
),
5,10,const Color(0xff999999),30),
),
),
),
));
///大于30,截取
String interceptString(String? s){
if(s == null || s.length == 0) return "";
if(s.length > 30) return s.substring(0,30);
return s;
}
hideOverlay(){
_overlayEntry?.remove();
_overlayEntry = null;
}
}
底部弹窗核心代码
void showLableDialog(
List<BorderBean>? customList, List<BorderBean>? systemList) {
///设置一个最小高度和一个最大高度
debugPrint("scaleH 是多少:$scaleH");
List<Widget> customChildren = generateList(customList);
List<Widget> systemChildren = generateList(systemList);
if(customChildren.length == 0 && systemChildren.length == 0) return ;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
constraints: BoxConstraints(minHeight: 200.h, maxHeight: 1.sh * 3 / 4),
builder: (BuildContext context) {
return Container(
width: 1.sw,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40.w),
topRight: Radius.circular(40.w))),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: EdgeInsets.all(13.5.h * scaleH),
child: Row(children: [
Expanded(
child: Container(
alignment: Alignment.center,
child: Text(
"标签",
style: blackStyle_16,
),
),
),
InkWell(
onTap: () => Navigator.of(context).pop(),
child: Image(
image: AssetImage("assets/images/close_btn.png")),
)
]),
),
dividerLineD8(),
Expanded(
child: Container(
width: 1.sw,
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"自定义标签",
style: blackMeduimStyle_16,
),
SizedBox(
height: 12.h * scaleH,
),
Container(
constraints: BoxConstraints(
minHeight: 100.h, maxHeight: 400.h),
child: SingleChildScrollView(
child: Wrap(
runSpacing: 8.h * scaleH,
children: customChildren,
),
),
),
Text(
"系统标签",
style: blackMeduimStyle_16,
),
SizedBox(
height: 12.h * scaleH,
),
Expanded(
child: SingleChildScrollView(
child: Wrap(
runSpacing: 8.h * scaleH,
children: systemChildren,
),
),
),
],
),
),
)
],
),
);
});
}
最终实现图
![](https://img.haomeiwen.com/i4163152/bbef4a5eb0ecf67b.png)
网友评论