简介
企业微信截图_706b7623-2225-4cf5-af4e-38389435973b.png这样的浮层该怎么实现?
方案1
bruno插件
这是一整套的UI库,其中的组件BrnPopupWindow可以大致实现。
- 实现方式是盖了一层弹出页面。
- 弹出路由自定义
class BrnPopupRoute extends PopupRoute {
final Duration _duration = Duration(milliseconds: 200);
Widget child;
BrnPopupRoute({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;
}
- 目标位置通过GlobalKey得到
// 获取targetView的位置
Rect _getWidgetGlobalRect(GlobalKey key) {
try {
BuildContext? ctx = key.currentContext;
RenderObject? renderObject = ctx?.findRenderObject();
RenderBox renderBox = renderObject as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
return Rect.fromLTWH(
offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
} catch (e) {
debugPrint('获取尺寸信息异常');
return Rect.zero;
}
}
- 三角形通过画笔画出来
// 绘制箭头
class _TrianglePainter extends CustomPainter {
bool isDownArrow;
Color color;
Color borderColor;
_TrianglePainter({
required this.isDownArrow,
required this.color,
required this.borderColor,
});
@override
void paint(Canvas canvas, Size size) {
Path path = Path();
Paint paint = Paint();
paint.strokeWidth = 2.0;
paint.color = color;
paint.style = PaintingStyle.fill;
if (isDownArrow) {
path.moveTo(0.0, -1.5);
path.lineTo(size.width / 2.0, size.height);
path.lineTo(size.width, -1.5);
} else {
path.moveTo(0.0, size.height + 1.5);
path.lineTo(size.width / 2.0, 0.0);
path.lineTo(size.width, size.height + 1.5);
}
canvas.drawPath(path, paint);
Paint paintBorder = Paint();
Path pathBorder = Path();
paintBorder.strokeWidth = 0.5;
paintBorder.color = borderColor;
paintBorder.style = PaintingStyle.stroke;
if (isDownArrow) {
pathBorder.moveTo(0.0, -0.5);
pathBorder.lineTo(size.width / 2.0, size.height);
pathBorder.lineTo(size.width, -0.5);
} else {
pathBorder.moveTo(0.5, size.height + 0.5);
pathBorder.lineTo(size.width / 2.0, 0);
pathBorder.lineTo(size.width - 0.5, size.height + 0.5);
}
canvas.drawPath(pathBorder, paintBorder);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
- 整个看下来,要实现这个弹窗还真不容易,特别是位置调整,还真是麻烦。
方案2:
-
可以通过
OverlayEntry
来展示浮层; -
展示浮层中的
context
不能用Get.context
替代 -
定位可以通过
GlobalKey
来完成,拿到目标的位置(Rect
)
/// 获取targetView的位置
Rect _getWidgetGlobalRect(GlobalKey key) {
try {
BuildContext? ctx = key.currentContext;
RenderObject? renderObject = ctx?.findRenderObject();
RenderBox renderBox = renderObject as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
return Rect.fromLTWH(
offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
} catch (e) {
debugPrint('获取尺寸信息异常');
return Rect.zero;
}
}
三角形画法
-
没有现成的
Widget
可以用,需要用CustomPaint
画出来 -
暂时不考虑空心的三角形,只考虑实心的三角形,所以
style
固定为PaintingStyle.fill
就可以,默认也是这个,不设置也行。strokeWidth
对于实心三角形没有意义,不需要设置 -
对外的参数,只需要一个填充颜色就可以了
-
三角形只要三个点,通过一笔画就可以了。可以想象为在一个矩形中一笔画一个三角形,x和y分别为矩形的宽和高。
Path getTrianglePath(double x, double y) {
return Path()
..moveTo(0, y)
..lineTo(x / 2, 0)
..lineTo(x, y)
..lineTo(0, y);
}
- 上面的是顶点朝上的三角形。顶点可以有4个方向,所以用一个枚举类型表示
/// 三角形顶点的朝向
enum TriangleDirection {
up,
down,
left,
right,
}
- 所以三角形的画笔类应该是这样的
class TrianglePainter extends CustomPainter {
final Color fillColor;
final TriangleDirection direction;
TrianglePainter({
required this.fillColor,
this.direction = TriangleDirection.up,
});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = fillColor
..style = PaintingStyle.fill;
canvas.drawPath(getTrianglePath(size.width, size.height), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
Path getTrianglePath(double x, double y) {
/// 朝下
if (direction == TriangleDirection.down) {
return Path()
..moveTo(0, 0)
..lineTo(x / 2, y)
..lineTo(x, 0)
..lineTo(0, 0);
}
/// 朝左
if (direction == TriangleDirection.left) {
return Path()
..moveTo(0, 0)
..lineTo(x, y / 2)
..lineTo(0, y)
..lineTo(0, 0);
}
/// 朝右
if (direction == TriangleDirection.right) {
return Path()
..moveTo(x, 0)
..lineTo(0, y / 2)
..lineTo(x, y)
..lineTo(x, 0);
}
/// 默认返回朝上的
return Path()
..moveTo(0, y)
..lineTo(x / 2, 0)
..lineTo(x, y)
..lineTo(0, y);
}
}
三角形定位
-
三角形的顶点向下,所以方向
direction
参数给TriangleDirection.down
; -
三角形的大小可以固定,这个参数由
CustomPaint
的size
参数给出; -
三角形的顶点应该是目标矩形上边的中点。结合三角形矩形的大小,可以算出三角形矩形左上角顶点,然后给
Positioned的left
和top
参数,就能定位三角形。
/// 获取目标矩形的定位
Rect targetRect = _getWidgetGlobalRect(key);
/// 定位三角形
const double triangleWith = 8.5;
const double triangleHeight = 5;
/// 三角形的顶点是目标矩形的中点
var trianglePoint = targetRect.topCenter;
/// 计算三角形左上角位置
var triangleLeft = trianglePoint.dx - (triangleWith / 2);
var triangleTop = trianglePoint.dy - triangleHeight;
测试界面
-
目标矩形用红色表示
-
倒立的三角形用蓝色表示
-
浮层代码如下:
/// 显示浮层,这里的context不能用Get.context;原因未知
void showOverlay({
required BuildContext context,
required GlobalKey key,
}) {
/// 获取目标矩形的定位
Rect targetRect = _getWidgetGlobalRect(key);
/// 定位三角形
const double triangleWith = 8.5;
const double triangleHeight = 5;
/// 三角形的顶点是目标矩形的中点
var trianglePoint = targetRect.topCenter;
/// 计算三角形左上角位置
var triangleLeft = trianglePoint.dx - (triangleWith / 2);
var triangleTop = trianglePoint.dy - triangleHeight;
/// 定义浮层
_overlayEntry = OverlayEntry(
builder: (context) {
return ExcludeSemantics(
excluding: true,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_overlayEntry.remove();
},
child: Material(
color: Colors.transparent,
child: Stack(
children: <Widget>[
/// 目标矩形
Positioned(
left: targetRect.left,
top: targetRect.top,
width: targetRect.width,
height: targetRect.height,
child: Container(
color: Colors.red,
),
),
/// 顶点在目标矩形上边中点的向下的三角形
Positioned(
left: triangleLeft,
top: triangleTop,
child: CustomPaint(
size: const Size(triangleWith, triangleHeight),
painter: TrianglePainter(
fillColor: Colors.blue,
direction: TriangleDirection.down,
),
),
),
],
),
),
),
);
},
);
Overlay.of(context).insert(_overlayEntry);
}
- 效果图如下:
文本
用一个Container
就可以了,高度确定,底部与三角形的顶部重合,定位不难。
最终的代码
class PandaTip {
/// 使用单例方式调用;用到成员变量的时候能够提供方便
PandaTip._();
static final PandaTip _instance = PandaTip._();
static PandaTip get instance => _instance;
/// 浮层
late OverlayEntry _overlayEntry;
/// 显示浮层,这里的context不能用Get.context;原因未知
void showOverlay({
required BuildContext context,
required GlobalKey key,
}) {
/// 获取目标矩形的定位
Rect targetRect = _getWidgetGlobalRect(key);
/// 定位三角形
const double triangleWith = 8.5;
const double triangleHeight = 5;
/// 三角形的顶点是目标矩形的中点
var trianglePoint = targetRect.topCenter;
/// 计算三角形左上角位置
var triangleLeft = trianglePoint.dx - (triangleWith / 2);
var triangleTop = trianglePoint.dy - triangleHeight;
/// 定位消息框;底部和三角形矩形顶部重合
const double messageHeight = 36;
const double messageLeft = 5;
double messageBottom = triangleTop;
double messageTop = messageBottom - messageHeight;
/// 定义浮层
_overlayEntry = OverlayEntry(
builder: (context) {
return ExcludeSemantics(
excluding: true,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_overlayEntry.remove();
},
child: Material(
color: Colors.transparent,
child: Stack(
children: <Widget>[
/// 目标矩形
Positioned(
left: targetRect.left,
top: targetRect.top,
width: targetRect.width,
height: targetRect.height,
child: Container(
color: Colors.red,
),
),
/// 顶点在目标矩形上边中点的向下的三角形
Positioned(
left: triangleLeft,
top: triangleTop,
child: CustomPaint(
size: const Size(triangleWith, triangleHeight),
painter: TrianglePainter(
fillColor: Colors.blue,
direction: TriangleDirection.down,
),
),
),
/// 消息矩形
Positioned(
left: messageLeft,
top: messageTop,
child: Container(
height: messageHeight,
padding: const EdgeInsets.symmetric(
horizontal: 30, vertical: 10),
decoration: BoxDecoration(
color: Colors.yellow,
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Text('请阅读并同意后再提交'),
],
),
),
),
],
),
),
),
);
},
);
Overlay.of(context).insert(_overlayEntry);
}
/// 获取targetView的位置
Rect _getWidgetGlobalRect(GlobalKey key) {
try {
BuildContext? ctx = key.currentContext;
RenderObject? renderObject = ctx?.findRenderObject();
RenderBox renderBox = renderObject as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
return Rect.fromLTWH(
offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
} catch (e) {
debugPrint('获取尺寸信息异常');
return Rect.zero;
}
}
}
- 最终的样子:
网友评论