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,
  );
}

运行效果:

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

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

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

相关推荐
子春一14 分钟前
Flutter for OpenHarmony:构建一个 Flutter 贪吃蛇游戏,深入解析状态机、碰撞检测与响应式游戏循环
flutter·游戏
2601_9495430115 分钟前
Flutter for OpenHarmony垃圾分类指南App实战:主题配置实现
android·flutter
2601_949833391 小时前
flutter_for_openharmony口腔护理app实战+知识实现
android·javascript·flutter
晚霞的不甘1 小时前
Flutter for OpenHarmony从基础到专业:深度解析新版番茄钟的倒计时优化
android·flutter·ui·正则表达式·前端框架·鸿蒙
ujainu2 小时前
无物理引擎实现吸附轨道逻辑 —— Flutter + OpenHarmony 实战指南
flutter·游戏·openharmony
kirk_wang2 小时前
Flutter艺术探索-Flutter地图与定位:google_maps_flutter与geolocator
flutter·移动开发·flutter教程·移动开发教程
mocoding2 小时前
使用专业的 Flutter 天气图标库weather_icons统一风格的图标,提升鸿蒙版天气预报应用专业度
flutter
ujainu2 小时前
Flutter + OpenHarmony 游戏开发进阶:动态关卡生成——随机圆环布局算法
算法·flutter·游戏·openharmony
2603_949462102 小时前
Flutter for OpenHarmony 社团管理App实战 - 资产管理实现
开发语言·javascript·flutter
小哥Mark2 小时前
各种Flutter拖拽交互组件助力鸿蒙应用个性化
flutter·交互·harmonyos