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

运行效果:

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

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

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

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

相关推荐
灰灰勇闯IT1 小时前
Flutter×VS Code:跨端开发的高效协作指南(2025最新配置)
笔记·flutter·harmonyos
●VON2 小时前
Flutter vs React Native vs 原生开发:有何不同?
学习·flutter·react native·react.js·openharmony
白茶三许2 小时前
【OpenHarmony】深入理解 Flutter 异步编程:从基础到实战
flutter·开源·openharmony·gitcode
西西学代码2 小时前
flutter---日历
flutter
kirk_wang2 小时前
Flutter 桌面/Web 开发:用 MouseRegion 打造原生级交互体验
前端·flutter·交互
●VON2 小时前
从零开始:用 Flutter 构建一个简洁高效的待办事项应用V1.0.0
学习·flutter·arm·openharmony·开源鸿蒙
●VON2 小时前
Flutter for OpenHarmony前置知识《Flutter 基础组件初探:第一章》
学习·flutter·跨平台·开发·openharmony·开源鸿蒙
恋猫de小郭2 小时前
用 AI 做了几个超炫酷的 Flutter 动画,同时又差点被 AI 气死
前端·flutter·aigc
晚霞的不甘2 小时前
分布式能力实战:Flutter + OpenHarmony 的跨设备协同开发
分布式·flutter