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

运行效果:

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

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

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

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

相关推荐
奋斗的小青年!!16 小时前
Flutter浮动按钮在OpenHarmony平台的实践经验
flutter·harmonyos·鸿蒙
程序员老刘20 小时前
一杯奶茶钱,PicGo + 阿里云 OSS 搭建永久稳定的个人图床
flutter·markdown
奋斗的小青年!!1 天前
OpenHarmony Flutter 拖拽排序组件性能优化与跨平台适配指南
flutter·harmonyos·鸿蒙
小雨下雨的雨1 天前
Flutter 框架跨平台鸿蒙开发 —— Stack 控件之三维层叠艺术
flutter·华为·harmonyos
行者961 天前
OpenHarmony平台Flutter手风琴菜单组件的跨平台适配实践
flutter·harmonyos·鸿蒙
小雨下雨的雨1 天前
Flutter 框架跨平台鸿蒙开发 —— Flex 控件之响应式弹性布局
flutter·ui·华为·harmonyos·鸿蒙系统
cn_mengbei1 天前
Flutter for OpenHarmony 实战:CheckboxListTile 复选框列表项详解
flutter
cn_mengbei1 天前
Flutter for OpenHarmony 实战:Switch 开关按钮详解
flutter
奋斗的小青年!!1 天前
OpenHarmony Flutter实战:打造高性能订单确认流程步骤条
flutter·harmonyos·鸿蒙
Coder_Boy_1 天前
Flutter基础介绍-跨平台移动应用开发框架
spring boot·flutter