Flutter使用Draggable與自定義RenderObject實現圖片新增標籤,隨意拖動位置效果

阿鍾發表於2021-08-27

本篇文章這裡主要是講一下整個功能的一個實現思路和使用到的技術點,更加詳細的還請參閱原始碼

一、實現的效果圖如下

Flutter使用Draggable與自定義RenderObject實現圖片新增標籤,隨意拖動位置效果 Flutter使用Draggable與自定義RenderObject實現圖片新增標籤,隨意拖動位置效果 Flutter使用Draggable與自定義RenderObject實現圖片新增標籤,隨意拖動位置效果 Flutter使用Draggable與自定義RenderObject實現圖片新增標籤,隨意拖動位置效果

二、實現的功能與使用到技術點

  • 功能
    • 標籤拖動的時候顯為一個圓點
    • 標籤只能在圖片顯示的範圍內拖動
    • 標籤可以拖動到指定位置刪除
    • 標籤拖動到左邊或者右邊,根據剩餘寬度自動改變標籤佈局方向
  • 技術點
    • Draggable拖動元件
    • 自定義RenderObjectRenderBox 參與元件繪製、擺放流程
    • 圖片使用BoxFit後,計算在容器內的實際位置

三、圖片真實大小、在螢幕上的位置獲取

  • 對效果圖進行分析,標註利於描述
Flutter使用Draggable與自定義RenderObject實現圖片新增標籤,隨意拖動位置效果
  • 元件的拖動效果實現
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)在圖片上所佔的比例;然後用比例值和顯示時圖片的大小計算標籤顯示的位置

相關文章