Flutter系列 - 点击放大图片

778 阅读3分钟

本文将讲解如何通过 Flutter 实现点击放大图片的功能。首先,我们根据之前的文章Flutter系列 - 开始你的第一个应用新建一个名为 flutter_scale_img 的应用。

flutter create flutter_scale_img

然后我们通过 Android Studio 启动 macOS(desktop) 的应用。

macOS_desktop.png

启动之后,是我们很熟悉的一个 counter 效果👇:

counter-effect.png

接下来,我们就是改写 main.dart 文件,实现点击放大图片的效果

shy.jpg

如果不想在本地开启一个应用的话,可以通过在线 IDE 实现效果,推荐 flutterlab

我们准备的图片资源是 AI 生成的图片👇

background.webp

👌,下面,我们一步一步来实现,文末会展示完整的 main.dart 的代码。

Widget 上获取鼠标点相对自身 dx 和 dy

在指定的挂件上点击,获取当前的鼠标点相对该挂件左上角的位置 dxdy。我们可以通过 localPosition 来实现:

class _MyHomePageState extends State<MyHomePage> {

  double dx = 0.0;
  double dy = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          width: 200.0,
          height: 200.0,
          color: Colors.blue,
          child: GestureDetector(
            onTapDown: (TapDownDetails details) {
              Offset localPosition = details.localPosition;
              setState(() {
                dx = localPosition.dx;
                dy = localPosition.dy;
              });
            },
            child: Text('点击的相对位置 $dx, $dy'),
          ),
        ),
        ),
    );
  }
}

当我们在 Container 这个挂件上点击的时候,会获取到该点相对其左上角的偏移坐标,效果如下:

dx_dy.gif

⚠️ 关于 GestureDetector 挂件,内容比较丰富也很实用,我们后面会单独出一篇文章来讲解

溢出隐藏

我们放大图片,在图片太大,会超出容器,但是我们又不需要展示溢出部分,所以需要对溢出的部分进行隐藏。

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            // 正常比例的图
            Container(
              width: 200.0,
              height: 200.0,
              decoration: BoxDecoration(
                  border: Border.all(
                    width: 2,
                    color: Colors.red,
                  )
              ),
              child: Transform.scale(
                scale: 1,
                child: const Image(
                  image: AssetImage('assets/images/background.webp'),
                  fit: BoxFit.cover,
                ),
              ),
            ),
            const SizedBox(
              width: 100.0,
            ),
            // 溢出
            Container(
              width: 200.0,
              height: 200.0,
              decoration: BoxDecoration(
                  border: Border.all(
                    width: 2,
                    color: Colors.red,
                  )
              ),
              child: Transform.scale(
                scale: 2,
                child: const Image(
                  image: AssetImage('assets/images/background.webp'),
                  fit: BoxFit.cover,
                ),
              ),
            ),
            const SizedBox(
              width: 100.0,
            ),
            // 隐藏溢出部分
            Container(
              width: 200.0,
              height: 200.0,
              decoration: BoxDecoration(
                  border: Border.all(
                    width: 2,
                    color: Colors.red,
                  )
              ),
              child: ClipRect(
                child: Transform.scale(
                  scale: 2,
                  child: const Image(
                    image: AssetImage('assets/images/background.webp'),
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            ),

          ],
        ),
        ),
    );
  }
}

上面的代码,效果如下👇

overflow.png

图片资源,我们直接引用了本地的资源,这需要在根目录下新建 assets/images 文件夹,并在 pubspec.yaml 下配置:

flutter:
  assets:
    - assets/images/

当然,为了方便,读者可以直接直接通过 Image.network(url) 来引用线上的图片资源。

上图中,从左到右的效果是:

  • 左边:正常比例的图,缩放是 1
  • 中间:正常比例的图,缩放是 2
  • 右边:正常比例的图,缩放是 2,通过 ClipRect 挂件对溢出的内容部分进行隐藏

更改原点

当我们放大图片的时候,我们是针对图片的原点 origin 进行放大的,所以,我们需要变更下图片的原点。

class _MyHomePageState extends State<MyHomePage> {

  double dx = 100.0;
  double dy = 100.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onTapDown: (TapDownDetails details) {
            Offset localPosition = details.localPosition;
            setState(() {
              dx = localPosition.dx;
              dy = localPosition.dy;
            });
          },
          child: Transform.scale(
            scale: 1,
            origin: Offset(dx, dy),
            child: Stack(
              children: [
                Container(
                  width: 200.0,
                  height: 200.0,
                  color: Colors.blue,
                ),
                Positioned(
                  top: dy - 6.0 / 2,
                  left: dx - 6.0 / 2,
                  child: Container(
                    width: 6.0,
                    height: 6.0,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

在上面的代码中,我们进行了 Position 定位,为了就是在视觉上看到原点的变化。这里的原点更改即鼠标点击点的位置。

origin-change.gif

最终效果

嗯~点击放大图片我们使用到技术点,上面已经分解讲解完成。

现在,我们整合上面的知识点。实现如下的效果👇

scale-real-effect.gif

整个 main.dart 文件的代码如下👇

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  // 该挂件是应用的根挂件
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter scale image',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  double offsetOriginX = 0.0;
  double offsetOriginY = 0.0;
  double scale = 1.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ClipRect(
          child: GestureDetector(
            onTapDown: (TapDownDetails details) {
              Offset localPosition = details.localPosition;

              // 点击点相对原点的实际距离,非缩放后的距离
              double diffDx = (localPosition.dx - (200.0 / 2 + offsetOriginX)) / scale;
              double diffDy = (localPosition.dy - (200.0 / 2 + offsetOriginY)) / scale;

              setState(() {
                scale = scale * 1.5;
                offsetOriginX = offsetOriginX + diffDx;
                offsetOriginY = offsetOriginY + diffDy;
              });
            },
            child: Container(
              width: 200.0,
              height: 200.0,
              decoration: BoxDecoration(
                  border: Border.all(
                    width: 2.0,
                    color: Colors.red,
                  )
              ),
              child: Transform.scale(
                scale: scale,
                origin: Offset(offsetOriginX, offsetOriginY),
                child: const Image(
                  image: AssetImage('assets/images/background.webp'),
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ),
        )
      ),
    );
  }
}

上面的代码中,我们需要留意计算 origin,因为这里我们只是需要计算针对 origin (200.0 / 2, 200.0 / 2) 的相对偏移,所以,我们需要考虑实际的相对位移即可。

【完✅】感谢阅读🌹