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

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

Flutter: 3.35.6

之前使用 html + javascript 实现了类似的功能,部分地方可以借鉴一下,现在基于此在flutter端实现一个容器内部元素可拖动,缩放,旋转的功能。

这个功能主要用于某个场景的装饰(例如家园,有各种家具,用户可以操作它们的摆放位置),组合图片(将多个图片在容器内随意摆放,组合成一张图)等等。所以还是比较实用的一个小功能。

依然是一个一个功能分析实现,首先来看平移。

一般来说,我们要移动这个元素,响应这个操作的区域大概率是在这个元素的内部,而且需要移动,所以我们就知道了响应移动操作是按下这个元素的内部区域并移动(元素内部按下移动响应移动操作)。为了响应这个操作,查阅文档,可以知道 GestureDetector 就可以满足,接下来初步体验一下:

dart 复制代码
import 'package:flutter/material.dart';

class TestGestureDetector extends StatelessWidget {
  const TestGestureDetector({super.key});

  void _onPanDown(DragDownDetails details) {
    print('按下: $details');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    print('更新: $details');
  }

  void _onPanEnd(DragEndDetails details) {
    print('抬起: $details');
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanDown: _onPanDown,
      onPanUpdate: _onPanUpdate,
      onPanEnd: _onPanEnd,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.amber,
      ),
    );
  }
}

运行效果:

可以看到手势的监听满足我们的需求,基于此开始实现一个元素的拖动。

限制在一个范围内拖动元素,使用 Stack + Positioned 来实现这样的元素,通过拖动改变 Positioned 的 top 值和 left 值:

dart 复制代码
import 'package:flutter/material.dart';

class DragContainer extends StatefulWidget {
  const DragContainer({super.key});

  @override
  State<DragContainer> createState() => _DragContainerState();
}

class _DragContainerState extends State<DragContainer> {
  double x = 10;
  double y = 10;

  void _onPanDown(DragDownDetails details) {
    print('按下: $details');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    print('更新: $details');

    // delta 表示自上次更新以来的位置偏移量
    setState(() {
      x += details.delta.dx;
      y += details.delta.dy;
    });
  }

  void _onPanEnd(DragEndDetails details) {
    print('抬起: $details');
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 300,
      height: 600,
      color: Colors.blueAccent,
      child: Stack(
        children: [
          Positioned(
            left: x,
            top: y,
            child: GestureDetector(
              onPanDown: _onPanDown,
              onPanUpdate: _onPanUpdate,
              onPanEnd: _onPanEnd,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.amber,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

运行效果:

就这样我们实现了一个简单的元素可拖动效果,从上面我们不难发现,这个元素也可以拖到容器外面,如果元素完全拖动到容器外,那我们就完全无法看到并操作这个元素了,所以意义不大,所以我们限制只允许在容器内拖动。我们接着更改代码:

dart 复制代码
import 'package:flutter/material.dart';

class DragContainer extends StatefulWidget {
  const DragContainer({super.key});

  @override
  State<DragContainer> createState() => _DragContainerState();
}

class _DragContainerState extends State<DragContainer> {
  /// 抽取容器的宽
  final double containerWidth = 300;
  /// 抽取容器的高
  final double containerHeight = 600;

  /// 抽取元素的宽
  double elementWidth = 100;
  /// 抽取元素的高
  double elementHeight = 100;
  double x = 10;
  double y = 10;

  void _onPanDown(DragDownDetails details) {
    print('按下: $details');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    print('更新: $details');

    setState(() {
      x += details.delta.dx;
      y += details.delta.dy;

      // 限制左边界
      if (x < 0) {
        x = 0;
      }
      // 限制右边界
      if (x > containerWidth - elementWidth) {
        x = containerWidth - elementHeight;
      }
      // 限制上边界
      if (y < 0) {
        y = 0;
      }
      // 限制下边界
      if (y > containerHeight - elementWidth) {
        y = containerHeight - elementHeight;
      }
    });
  }

  void _onPanEnd(DragEndDetails details) {
    print('抬起: $details');
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: containerWidth,
      height: containerHeight,
      color: Colors.blueAccent,
      child: Stack(
        children: [
          Positioned(
            left: x,
            top: y,
            child: GestureDetector(
              onPanDown: _onPanDown,
              onPanUpdate: _onPanUpdate,
              onPanEnd: _onPanEnd,
              child: Container(
                width: elementWidth,
                height: elementHeight,
                color: Colors.amber,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

运行效果:

现在我们对边界做出了限制,但新的问题又出现了,我们手指拖动到容器外面,依然在响应事件,此时移动到容器内的过程中,元素也在跟着移动,待完全将手指移动到容器内,元素和手指已经隔了很长一个距离了。

这是因为 delta 中记录着自上次更新的偏移量,即便是手指在容器外,依然会记录这个值,从而实时的更改这个值,当我们手指在某一个方法超出范围时,继续向那个范围外移动,元素因为限制所以不会超出容器外,指针和元素越离越远,当我们向回走时,y += details.delta.dy; (以下边界为例),dy 为负值,y + dy 就小于了边界值,所以就直接同步移动,这就导致了指针和元素有了一定的距离。

为了解决这个问题,我们可以使用下面这种方法,将手指按下,移动,抬起看做一个操作动作,用变量记录当前的初始的x和y,手指按下记录按下的坐标,手指移动时记录移动到的坐标,通过按下的坐标与移动的坐标计算移动的距离,那么就有:当前的值 = 初始化值 + (手指当前移动的值 - 手指按下的值);在当次操作动作过程中,我们始终以当前初始的x和y为标准,以按下的位置做和移动的位置来计算,这样即便是移动到容器外再往回移动的过程中,使用初始的x和y、初始按下位置和移动的位置(只要移动的位置还是在边界外)计算出来依然大于边界值。这样就达到了我们的要求。那么新的问题来了,我们怎么拿到按下的坐标和移动的坐标呢?我们看看打印的值有些什么:

可以看到还有以下的值供我们使用:

  • globalPosition: 提供指针在全局坐标系中的当前位置坐标
  • localPosition: 表示指针相对于当前监听组件坐标系的位置坐标

所以我们只需要计算出当前位置即可,这里使用 localPosition 来计算:

dart 复制代码
import 'package:flutter/material.dart';

class DragContainer extends StatefulWidget {
  const DragContainer({super.key});

  @override
  State<DragContainer> createState() => _DragContainerState();
}

class _DragContainerState extends State<DragContainer> {
  /// 抽取容器的宽
  final double containerWidth = 300;
  /// 抽取容器的高
  final double containerHeight = 600;

  /// 抽取元素的宽
  double elementWidth = 100;
  /// 抽取元素的高
  double elementHeight = 100;
  double x = 10;
  double y = 10;
  double initX = 10;
  double initY = 10;
  Offset startPosition = Offset(0, 0);

  void _onPanDown(DragDownDetails details) {
    print('按下: $details');
    setState(() {
      startPosition = details.localPosition;
    });
  }

  void _onPanUpdate(DragUpdateDetails details) {
    print('更新: $details');

    setState(() {
      // 计算方法
      x = initX + details.localPosition.dx - startPosition.dx;
      y = initY + details.localPosition.dy - startPosition.dy;

      // 限制左边界
      if (x < 0) {
        x = 0;
      }
      // 限制右边界
      if (x > containerWidth - elementWidth) {
        x = containerWidth - elementWidth;
      }
      // 限制上边界
      if (y < 0) {
        y = 0;
      }
      // 限制下边界
      if (y > containerHeight - elementHeight) {
        y = containerHeight - elementHeight;
      }
    });
  }

  void _onPanEnd() {
    print('抬起或者因为某些原因并没有触发onPanDown事件');
    setState(() {
      // 当次结束后重新记录,也可以在按下时记录
      initX = x;
      initY = y;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: containerWidth,
      height: containerHeight,
      color: Colors.blueAccent,
      child: Stack(
        children: [
          Positioned(
            left: x,
            top: y,
            child: GestureDetector(
              onPanDown: _onPanDown,
              onPanUpdate: _onPanUpdate,
              onPanEnd: (details) => _onPanEnd,
              onPanCancel: () => _onPanEnd,
              child: Container(
                width: elementWidth,
                height: elementHeight,
                color: Colors.amber,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

运行效果:

可以看到超出边界往回走的时候指针计算的位置依然大于边界值,直到回到按下的地方相对位置(按下时相对于黄色区域的位置坐标)才重新移动。

这样我们就初步实现了元素的移动,这又是一个系列的文章,后面再慢慢实现其他的功能。

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

至此该篇结束,感谢阅读~拜拜~

相关推荐
renke33642 分钟前
Flutter 2025 国际化与本地化工程体系:打造真正全球化的应用体验
flutter
子春一220 分钟前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
renke336435 分钟前
Flutter 2025 状态管理工程体系:从 setState 到响应式架构,构建可维护、高性能的状态流
flutter
麦客奥德彪1 小时前
Flutter 性能优化完整指南
flutter
麦客奥德彪1 小时前
Flutter 布局组件选择指南
flutter
子春一22 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
renke33645 小时前
Flutter 2025 模块化与微前端工程体系:从单体到可插拔架构,实现高效协作、独立交付与动态加载的下一代应用结构
前端·flutter·架构
武玄天宗7 小时前
第三章、flutter项目启动时有一段时间出现白屏怎么办?
flutter
renke33648 小时前
Flutter 2025 跨平台工程体系:从 iOS/Android 到 Web/Desktop,构建真正“一次编写,全端运行”的产品
android·flutter·ios