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

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

Flutter: 3.35.7

前面我们抽取了区域的配置,主要实现了对内置区域的自定义,现在有个问题,如果是我们想自定义某个特定区域实现特定的效果,现在的数据都在部件的内部,如果不能提升到外部,那我们不能由外界传入貌似也没有什么用,因为功能需求都不一样,只有尽可能的以我现在的能力去进行封装。其实对于大多数的场景,修改内置的区域配置即可,我们就不需要考虑自定义这些,然而预留一些口子就可以极大地方便其他人使用。

这里就简单实现一下自定义区域,之前我们预留了自定义区域的fn,但那只是一个简单的占位,如果用户想自定义一些区域实现部分的功能,基本上就是对当前选中元素的操作,我们将可以给出的数据给到用户,让用户自行计算,并将计算完成的元素返回:

dart 复制代码
typedef AreaFunction = ElementModel Function({
  /// 点击的坐标
  required Offset tapPoint,
  /// 选中的元素
  required ElementModel element,
  /// 容器的宽度
  required double containerWidth,
  /// 容器的高度
  required double containerHeight,
  /// 移动的坐标
  Offset? movePoint,
});

接下来我们基于此实现一个简单的点击将元素移动到容器中心的功能,用这个例子来大概说明一下自定义区域的使用。我们之前实现了初始化区域配置的函数,现在加入自定义的区域,所以我们得对其进行改造:

dart 复制代码
/// 初始化响应区域
void _initArea() {
  // 初始为配置里面定义的
  List<ResponseAreaModel> areaList = [...ConstantsConfig.baseAreaList];

  if (widget.areaConfigList != null) {
    // 将外界传递的配置合并
    for (var area in widget.areaConfigList!) {
      final int index = areaList.indexWhere((item) => item.status == area.status);

      // 如果是内置的区域,则修改配置
      if (index > -1) {
        // 如果是不使用,则移除
        if (area.use == false) {
          areaList.removeAt(index);
        } else {
          // 否则进行修改配置
          areaList[index].copyWith(
            xRatio: area.xRatio,
            yRatio: area.yRatio,
            icon: area.icon,
            fn: area.fn,
          );
        }
      } else {
        // 如果是自定义的区域,我们默认该有的参数是存在的
        areaList.add(ResponseAreaModel(
          areaWidth: ConstantsConfig.minSize,
          areaHeight: ConstantsConfig.minSize,
          xRatio: area.xRatio!,
          yRatio: area.yRatio!,
          trigger: area.trigger,
          icon: area.icon!,
          status: area.status,
          fn: area.fn,
        ));
      }
    }
  }

  setState(() {
    _areaList = areaList;
  });
}

配置(已经让外界传入):

dart 复制代码
// 外界容器
// 其他省略...

late List<CustomAreaConfig> _customAreaList;

@override
void initState() {
  super.initState();

  _customAreaList = [
    // 不使用缩放区域
    CustomAreaConfig(
      status: ElementStatus.scale.value,
      use: false,
    ),
    // 将旋转移到右下角
    CustomAreaConfig(
      status: ElementStatus.rotate.value,
      xRatio: 1,
      yRatio: 1,
    ),
    // 测试自定义区域
    CustomAreaConfig(
      status: 'center',
      xRatio: 0,
      yRatio: 1,
      icon: 'assets/images/icon_center.png',
      trigger: TriggerMethod.down,
      fn: _centerFn,
    ),
  ];
}

/// 测试自定义区域的函数
ElementModel _centerFn({
  /// 点击的坐标
  required Offset tapPoint,
  /// 选中的元素
  required ElementModel element,
  /// 容器的宽度
  required double containerWidth,
  /// 容器的高度
  required double containerHeight,
  /// 移动的坐标
  Offset? movePoint,
}) {
  return ElementModel(
    id: element.id,
    elementWidth: element.elementWidth,
    elementHeight: element.elementHeight,
  );
}

// 其他省略...

Column(
  children: [
    Expanded(
      child: MultipleTransformContainer(
        // 传入配置
        areaConfigList: _customAreaList,
      ),
    ),
  ],
),

运行效果:

可以看到我们定义了左下角为自定义区域,其他内置的区域配置也生效了,接下来我们来实现这个区域的方法。在实现之前,我们得对按下事件函数、移动事件函数和更新属性函数进行逻辑新增,新增如果是点击的自定义区域,则响应自定义的方法,将必要参数传递过去:

dart 复制代码
/// 当前元素属性变化的时候更新列表中对应元素的属性
///
/// 因为可能是触发用户的自定义区域,
/// 所以如果是用户自定义的区域,则将对应元素的属性修改成用户计算后的元素属性
void _onChange({ElementModel? data}) {
  if (_currentElement == null || _temporary == null) return;

  final ElementModel? tempElement = data ?? _currentElement;
  for (var i = 0; i < _elementList.length; i++) {
    final ElementModel item = _elementList[i];

    if (item.id == tempElement?.id) {
      _elementList[i] = item.copyWith(
        x: tempElement?.x,
        y: tempElement?.y,
        elementWidth: tempElement?.elementWidth,
        elementHeight: tempElement?.elementHeight,
        rotationAngle: tempElement?.rotationAngle,
      );
      setState(() {});
      break;
    }
  }
}

/// 处理自定义事件
///
/// 通过当前状态[status]来确定是否是自定义区域, 如果是,
/// 则将按下坐标 [tapPoint], 移动坐标 [movePoint] (如果是移动状态),
/// 和当前元素[element]传递过去用于自定义的计算
void _onCustomFn({
  required ElementModel element,
  required Offset tapPoint,
  required String? status,
  Offset? movePoint,
}) {
  final int index = _areaList.indexWhere((item) => item.status == status);

  if (index > -1) {
    final ResponseAreaModel item = _areaList[index];

    if (item.fn != null) {
      final ElementModel data = item.fn!(
        tapPoint: tapPoint,
        element: element,
        movePoint: movePoint,
        containerHeight: _containerHeight,
        containerWidth: _containerWidth,
      );
      _onChange(data: data);
    }
  }
}

/// 按下事件
void _onPanDown(DragDownDetails details) {
  // 其他省略...

  // 新增判断
  // 如果当前有选中的元素且和点击区域的currentElement是一个元素
  // 并且 temp 的 status对应的触发方式为点击,那么就响应对应的点击事件
  if (currentElement?.id == _currentElement?.id && temp.trigger == TriggerMethod.down) {
    final Function? fn = _onElementStatus(x: dx, y: dy)[temp.status];

    if (fn != null) {
      fn();
    } else {
      // final int index = _areaList.indexWhere((item) => item.status == temp.status);
      //
      // if (index > -1) {
      //   final ResponseAreaModel item = _areaList[index];
      //
      //   if (item.fn != null) {
      //     final ElementModel data = item.fn!(
      //       tapPoint: Offset(dx, dy),
      //       element: currentElement!,
      //       containerHeight: _containerHeight,
      //       containerWidth: _containerWidth,
      //     );
      //     _onChange(data: data);
      //   }
      // }
      // 新增处理自定义函数
      _onCustomFn(
        element: currentElement!,
        tapPoint: Offset(dx, dy),
        status: temp.status,
      );
    }

    if (temp.status == ElementStatus.deleteStatus.value) {
      // 因为是删除,就置空选中,让下面代码执行最后的清除
      currentElement = null;
    }
  }

  // 其他省略...
}

/// 按下移动事件
void _onPanUpdate(DragUpdateDetails details) {
  // 其他省略...

  // if (_temporary?.trigger == TriggerMethod.move && fn != null) fn();
  if (_temporary?.trigger == TriggerMethod.move) {
    if (fn != null) {
      fn();
    } else {
      // 新增处理自定义函数
      _onCustomFn(
        element: _currentElement!,
        tapPoint: _startPosition,
        movePoint: Offset(x, y),
        status: _temporary?.status,
      );
    }
  }
}

接下来我们简单实现自定义区域的功能

dart 复制代码
/// 测试自定义区域的函数
ElementModel _centerFn({
  /// 点击的坐标
  required Offset tapPoint,
  /// 选中的元素
  required ElementModel element,
  /// 容器的宽度
  required double containerWidth,
  /// 容器的高度
  required double containerHeight,
  /// 移动的坐标
  Offset? movePoint,
}) {
  // 计算容器中心
  final double x = (containerWidth - element.elementWidth) / 2;
  final double y = (containerHeight - element.elementHeight) / 2;

  return ElementModel(
    id: element.id,
    elementWidth: element.elementWidth,
    elementHeight: element.elementHeight,
    x: x,
    y: y,
    rotationAngle: element.rotationAngle,
  );
}

运行效果:

这样我们就对自定义区域及事件做出了响应,虽然可能很少使用,但因为预留了这个口子而多了一丝灵活性。如果还需要实现其他自定义可能,可以按照类似的方式来实现。

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

好了,今天的分享就结束了,后续就开始实现容器属性的设置了,比如辅助线,比如撤销还原。感谢阅读~拜拜~

相关推荐
名字被你们想完了2 小时前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(八)
前端·flutter
renke33642 小时前
Flutter 2025 测试工程体系:从单元测试到生产验证,构建高可靠、可交付、零回归的工程质量防线
flutter
kirk_wang2 小时前
Flutter 鸿蒙项目 Android Studio 点击 Run 失败 ohpm 缺失
flutter·android studio·harmonyos
Swuagg3 小时前
Flutter 数据存储之 SharedPreferences 键值对存储
flutter·sp
Fate_I_C3 小时前
Flutter鸿蒙0-1开发-工具环境篇
flutter·华为·harmonyos·鸿蒙
走在路上的菜鸟3 小时前
Android学Dart学习笔记第二十四节 类-可调用对象Class()()
android·笔记·学习·flutter
2501_915921433 小时前
Flutter App 到底该怎么测试?如何在 iOS 上进行测试
android·flutter·ios·小程序·uni-app·cocoa·iphone
Fate_I_C4 小时前
Flutter鸿蒙0-1开发-flutter create <prjn>
flutter·华为·harmonyos·鸿蒙
走在路上的菜鸟4 小时前
Android学Dart学习笔记第二十五节 类修饰符
android·笔记·学习·flutter