Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(九)

Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(九)

Flutter: 3.35.7

今天的功能完成那基本功能就完成了,所以给出github链接:

text 复制代码
https://github.com/yhtqw/FrontEndDemo/tree/main/flutter_demo/lib/pages/transform_use/widgets/multiple_transform

前面我们简单实现了图片元素和文本元素的新增,优先实现的功能,很多地方还可以优化。今天我来讨论一下文本元素的拖动缩放。

对于文本元素,文本元素的高度是由文本自身的样式来决定的,所以我们限制文本元素只能缩放宽度,高度自动计算。这样就会对缩放区域进行元素类型判断,如果是文本元素,那么只缩放宽度,高度自适应;如果是其他元素,则同上对宽度高度进行缩放。既然功能又区别,那么icon也要有区别,那么之前的元素操作区域得做出更改,新增这种特殊功能区分元素类型对应图标不一样的配置:

dart 复制代码
class ResponseAreaModel {
  // 其他省略...

  /// 元素类型不同展示不同的操作icon
  final Map<String, String>? iconConfig;

  // 其他省略...
}

class ConstantsConfig {
  // 其他省略...

  static final List<ResponseAreaModel> baseAreaList = [
    // 其他省略...

    // 缩放
    ResponseAreaModel(
      areaWidth: 20,
      areaHeight: 20,
      xRatio: 1,
      yRatio: 1,
      status: ElementStatus.scale.value,
      icon: 'assets/images/icon_scale.png',
      trigger: TriggerMethod.move,
      iconConfig: {
        ElementType.textType.type: 'assets/images/icon_scale_text.png',
      },
    ),

    // 其他省略...
  ],

  // 其他省略...
}

// 其他省略...

// 如果选中,则展示操作区域
if (selected) ...areaList.map((item) => Positioned(
  top: elementItem.elementHeight * item.yRatio - item.areaHeight / 2,
  left: elementItem.elementWidth * item.xRatio - item.areaWidth / 2,
  child: Container(
    width: item.areaWidth,
    height: item.areaHeight,
    alignment: Alignment.center,
    decoration: BoxDecoration(
      color: Colors.blueAccent,
      borderRadius: BorderRadius.circular(item.areaWidth / 2),
    ),
    child: Image.asset(
      // 判断对应类型的图片是否单独存在
      item.iconConfig?[elementItem.type] ?? item.icon,
      width: item.areaWidth - ConstantsConfig.areaIconMargin,
      height: item.areaHeight - ConstantsConfig.areaIconMargin,
      fit: BoxFit.scaleDown,
      color: Colors.white,
    ),
  ),
)),

运行效果:

这样我们就通过元素状态区分了特殊功能的图标展示,现在就是功能的实现了:

dart 复制代码
/// 处理元素缩放
///
/// 通过移动点坐标[x]和[y]与按下的初始坐标,
void _onScale({required double x, required double y}) {
  if (_currentElement?.type == ElementType.textType.type) {
    _onScaleText(x: x, y: y);
  } else {
    _onScaleBase(x: x, y: y);
  }
}

/// 抽取获取缩放需要的基础参数
(double, double, double, double, double) _getScaleParams({required double x, required double y}) {
  final double oWidth = _temporary!.width;
  final double oHeight = _temporary!.height;
  final double oX = _temporary!.x;
  final double oY = _temporary!.y;
  final double resizeRatio = _calcResizeRatio(x: x, y: y);

  return (oWidth, oHeight, oX, oY, resizeRatio);
}

/// 处理非文本元素的缩放
void _onScaleBase({required double x, required double y}) {
  if (_currentElement == null || _temporary == null) return;
  
  final (oWidth, oHeight, oX, oY, resizeRatio) = _getScaleParams(x: x, y: y);
  double newW = oWidth * resizeRatio;
  double newH = oHeight * resizeRatio;
  final double minSize = ConstantsConfig.minSize;

  // 以短边为基准来计算最小宽高
  if (oWidth <= oHeight && newW < minSize) {
    newW = minSize;
    newH = minSize * oHeight / oWidth;
  } else if (oHeight < oWidth && newH < minSize) {
    newH = minSize;
    newW = minSize * oWidth / oHeight;
  }

  // 以长边为基准来计算最大宽高
  if (oWidth >= oHeight && newW >= _transformWidth) {
    newW = _transformWidth;
    newH = _transformWidth * oHeight / oWidth;
  } else if (oHeight > oWidth && newH >= _transformHeight) {
    newH = _transformHeight;
    newW = _transformHeight * oWidth / oHeight;
  }

  if (
    newW == _currentElement?.elementWidth &&
      newH == _currentElement?.elementHeight
  ) {
    return;
  }

  _currentElement = _currentElement?.copyWith(
    elementWidth: newW,
    elementHeight: newH,
    x: oX - (newW - oWidth) / 2,
    y: oY - (newH - oHeight) / 2,
  );
  _onChange();
}

/// 文本元素的缩放
void _onScaleText({required double x, required double y}) {
  if (_currentElement == null || _temporary == null) return;
  
  final (oWidth, oHeight, oX, oY, resizeRatio) = _getScaleParams(x: x, y: y);
  double newW = oWidth * resizeRatio;
  final double minSize = ConstantsConfig.minSize;

  // 以短边为基准来计算最小宽高
  if (oWidth <= oHeight && newW < minSize) {
    newW = minSize;
  }

  // 以长边为基准来计算最大宽高
  if (oWidth >= oHeight && newW >= _transformWidth) {
    newW = _transformWidth;
  }

  final TextStyle style = _getTextStyle(_currentElement!.textOptions!);
  final (tempWidth, tempHeight) = TransformUtils.calculateTextSize(
    text: _currentElement!.textOptions!.text,
    style: style,
    maxWidth: newW,
  );

  _currentElement = _currentElement?.copyWith(
    elementWidth: newW,
    elementHeight: tempHeight,
    x: oX - (newW - oWidth) / 2,
    y: oY - (tempHeight - oHeight) / 2,
  );
  _onChange();
}

运行效果:

这样我们就实现了文本元素的拉伸效果。可以看到,随着功能的增加,可能就需要对之前的逻辑做更改,不过因为抽取了配置,很多地方就只需要加入少量的代码即可实现部分功能。

基础的功能差不多就完成了,接下来主功能就还有一个,那就是保存,我们不知道这个功能是否还存在编辑,如果不存在,直接将该结构转成图片返回即可,所以我们保存就直接将数据和图片都传递过去即可。一般这种大量数据存后端都是JSON字符串,所以我们直接转成json字符串存储:

dart 复制代码
Future<void> _onSave() async {
  if (_isLoading) return;
  _isLoading = true;

  if (_currentElement != null) {
    setState(() {
      _currentElement = null;
    });
  }

  if (_currentElement == null) {
    final String imagePath = await _getImagePath();
    final List<Map<String, dynamic>> tempStringList = _elementList.map((item) => ElementModel.toJson(item)).toList();

    widget.onSave(imgSrc: imagePath, data: jsonEncode(tempStringList));

    _isLoading = false;
  } else {
    _isLoading = false;
    _onSave();
  }
}

运行效果:

这样我们就简单实现了图片的生成,并且将数据转换成JSON字符串传递给后端保存,后续后端返回这个字符串再还原就可以进行编辑。这样就简单实现了基础的功能,后续就实现辅助的功能。

感兴趣的也可以关注我的微信公众号【前端学习小营地】,不定时会分享一些小功能~

好了,今天的分享到此结束,感谢阅读~拜拜~

相关推荐
红尘散仙8 小时前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
陈随易9 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
ZhengEnCi9 小时前
Agent 工具怎么折叠海量输出才「爽」:Trae、Claude Code 的渐进式披露设计原理
前端·agent
IT_陈寒13 小时前
Redis内存暴涨,没想到是AOF重写捣的鬼
前端·人工智能·后端
ikoala13 小时前
让 Codex 自己查账:一个省 token 的小技巧
前端·后端·ai编程
ikoala13 小时前
AI 写页面像说明书,修 Bug 像贴膏药?把这两句话写进 AGENTS.md
前端·javascript·ai编程
好的佩奇14 小时前
Dart 之扩展方法
android·flutter·dart
去伪存真15 小时前
AI Agent Harness 深度测评与技术选型实战
前端·agent
好的佩奇16 小时前
蓝牙之经典蓝牙
android·flutter·蓝牙
FRing0318 小时前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2