本篇文章这里主要是讲一下整个功能的一个实现思路和使用到的技术点,更加详细的还请参阅源码
一、实现的效果图如下
二、实现的功能与使用到技术点
- 功能
- 标签拖动的时候显为一个圆点
- 标签只能在图片显示的范围内拖动
- 标签可以拖动到指定位置删除
- 标签拖动到左边或者右边,根据剩余宽度自动改变标签布局方向
- 技术点
Draggable 拖动组件- 自定义
RenderObject 、RenderBox 参与组件绘制、摆放流程 - 图片使用
BoxFit 后,计算在容器内的实际位置
三、图片真实大小、在屏幕上的位置获取
- 对效果图进行分析,标注利于描述
- 组件的拖动效果实现
Widget label = LabelWidget();
Draggable(
child: label,
feedback: Material(color: Colors.transparent, child: _feedbackWidget()),
childWhenDragging: Offstage(child: label),
onDragUpdate:(detail){}
onDragEnd: (detail){}
),
1、由于Draggable组件在拖动的时候,是可以在整个屏幕上进行拖动;所以在回调里拿到的Offset位置是相对于屏幕左上角的。 2、所以要判断拖动后的位置是否处于图片内,需要知道图片矩形在整个屏幕的位置信息;然后利用Rect的contains()函数判断点是否在矩形内即可。
1、 计算图片矩形在屏幕上的位置,由上图可知;我们需要先知道Stack 在屏幕上的位置与大小:
- 获取
Stack 在屏幕上的位置与大小,这个比较简单给stack设置个key,然后在第一帧绘制完进行获取
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((callback) {
containerSize = _getWidgetSize(_stackKey);
containerOffset = _location(_stackKey);
});
}
Size _getWidgetSize(GlobalKey key) {
return key.currentContext!.size!;
}
Offset _location(GlobalKey key) {
RenderBox? renderBox = key.currentContext!.findRenderObject() as RenderBox?;
return renderBox!.localToGlobal(Offset.zero);
}
- 通过上面操作:就已经获取到了上图标注的
A点 坐标了Offset A = containerOffset; - 那怎么获取图片的大小位置呢?
流程:获取图片大小 ——> 计算图片的宽高、高宽比 ——> 根据Stack容器大小计算图片真实显示大小 ——> 根据图片真实大小计算在Stack内的位置
ExtendedImage.network(
'图片地址',
fit: BoxFit.contain,
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.completed) {
state.extendedImageInfo?.image;
}
},
),
- 由于上面使用到了图片缩放模式
BoxFit.contain ,所以图片会根据给定的容器大小来显示图片;那么要怎么计算呢?这个答案就要从源码里找到答案了
- 代码位于
flutterSDk/packages/flutter/lib/src/painting/box_fit.dart 文件中的applyBoxFit 函数 inputSize 指的就是图片大小,outputSize 指的就是给定的容器大小里,有了计算公式就简单了
3、计算图片在给定容器内的真实大小
Size _calcImgSize(ui.Image? image) {
Size result = Size.zero;
double imageAspectRatio = image.width.toDouble() / image.height.toDouble();
double containerRatio = containerSize!.width / containerSize!.height;
if (containerRatio > imageAspectRatio) {
result =
Size(imageAspectRatio * containerSize!.height, containerSize!.height);
} else {
result = Size(containerSize!.width, image.height.toDouble() * containerSize!.width /
image.width.toDouble());
}
return result;
}
3、计算图片在容器内的位置,也就是上图B 点的位置;有了B点的位置也就能够得到图片实际位置在屏幕上的矩形了
图片是在给定的Stack中居中的,有了Stack和图片大小就可以计算到图片的起点坐标(imgStartOffset) 了
Size realImgSize = _calcImgSize(image);
double imgOffsetX = (containerSize!.width - realImgSize.width) / 2;
double imgOffsetY = (containerSize!.height - realImgSize.height) / 2;
Offset imgStartOffset = Offset(imgOffsetX, imgOffsetY);
Offset imgOffset = containerOffset + imgStartOffset;
Rect rect = imgOffset & realImgSize;
三、标签拖动效果实现
- 开始拖动的时候需要隐藏标签的文字部分,在拖动完成的时候在显示出来
- 标签只能拖动圆点,文字部分是不能拖动的
- 拖动结束的时候根据位置和剩余宽度决定文字居左还是居右
1、这里重点讲一下标签这个组件,自定义RenderObject 达到只能拖动圆点部分
从上图可以看出,Draggable 组件直接大小就只有红色部分,文字部分不占据任何大小;这样除了红色部分可以响应触摸事件,其它部分都是无法响应的。
class LabelWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return LabelWidgetState();
}
}
class LabelWidgetState extends State<LabelWidget> {
@override
Widget build(BuildContext context) {
return ShopGoodsLabelRenderObjectWidget();
}
}
class ShopGoodsLabelRenderObjectWidget extends SingleChildRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) {
return ShopGoodsLabelBox();
}
}
class ShopGoodsLabelBox extends RenderProxyBox with RenderProxyBoxMixin {
@override
void performLayout() {
super.performLayout();
realSize = size;
sizeCallback?.call(realSize);
size = circleSize;
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) context.paintChild(child!, offset);
}
}
- 重写
performLayout() 修改size的大小为上面红色矩形的大小就可以实现了,同时将真实大小通知出去;在拖动结束的时候判断标签文字部分是显示在左边还是右边需要使用到。 - 重写
paint() 绘制内容,当标签文字在左边显示的时候需要自己计算offset的值进行绘制
2、标签拖动位置计算,拖动删除处理
- 前面一开始已经将图片矩形在屏幕上的位置计算出来了,那只要在拖动结束的时候判断位置是否在矩形内即可,如下:
imgRect.contains(offset);
- 同理,标签删除也只需要知道删除矩形在屏幕上的位置就可以了
四、对添加好的标签进行展示
- 展示的逻辑和添加的逻辑基本一致
- 知道图片的宽高比、高宽比,用于计算图片的真实位置
- 计算标签点(x,y)在图片上所占的比例;然后用比例值和显示时图片的大小计算标签显示的位置
|