阅读 358

Flutter使用Draggable与自定义RenderObject实现图片添加标签,随意拖动位置效果

本篇文章这里主要是讲一下整个功能的一个实现思路和使用到的技术点,更加详细的还请参阅源码

一、实现的效果图如下

二、实现的功能与使用到技术点

  • 功能
    • 标签拖动的时候显为一个圆点
    • 标签只能在图片显示的范围内拖动
    • 标签可以拖动到指定位置删除
    • 标签拖动到左边或者右边,根据剩余宽度自动改变标签布局方向
  • 技术点
    • Draggable拖动组件
    • 自定义RenderObjectRenderBox 参与组件绘制、摆放流程
    • 图片使用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内的位置

2、 获取图片大小,这里使用到了extended_image库

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、计算图片在给定容器内的真实大小

///计算图片的真实大小
///[image] 图片模式
///[BoxFit.contain] 计算模式
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;
///这个就是B点的位置坐标(相对于A点位原点)
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);
    ///修改RenderObject的大小
    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)在图片上所占的比例;然后用比例值和显示时图片的大小计算标签显示的位置
文章分类
前端