首页 > 解决方案 > Flutter 使用 GestureDetectors 制作图像也可拖动

问题描述

我的目标是拥有一个image我可以在其中缩放和移动的CustomClipperImage,它也应该是Draggable

现在我可以scale在它的图像Clip,这看起来像这样:

屏幕录像

这是它的代码:

          child: Container(
              height: _containetWidth,
              width: _containetWidth,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(10.0),
                border: Border.all(color: Colors.white, width: 5),
              ),
              child: GestureDetector(
                onTap: () => print("tapped"),
                onScaleStart: (details) {
                  _startingFocalPoint.value = details.focalPoint;
                  _previousOffset.value = _offset.value;
                  _previousZoom.value = _zoom.value;
                },
                onScaleUpdate: (details) {
                  _zoom.value = _previousZoom.value * details.scale;
                  final Offset normalizedOffset =
                      (_startingFocalPoint.value - _previousOffset.value) /
                          _previousZoom.value;
                  _offset.value =
                      details.focalPoint - normalizedOffset * _zoom.value;
                },
                child: Stack(
                  children: [
                    ClipPath(
                      clipper: CustomClipperImage(),
                      child: Transform(
                        transform: Matrix4.identity()
                          ..translate(_offset.value.dx, _offset.value.dy)
                          ..scale(_zoom.value),
                        child: Image.asset('assets/images/example.jpg',
                            width: _containetWidth,
                            height: _containetWidth,
                            fit: BoxFit.fill),
                      ),
                    ),
                    CustomPaint(
                      painter: MyPainter(),
                      child: Container(
                          width: _containetWidth, height: _containetWidth),
                    ),
                  ],
                ),
              ),
            ),

但我做不到Draggable......我尝试将整个Container或仅包裹在Image.asset内部Draggable,但这样做时,scaling停止工作并且Draggable也不工作。

实现这一目标的最佳方法是什么?我在这方面找不到任何东西...如果您需要更多详细信息,请告诉我!

标签: imageflutterdartwidgetdraggable

解决方案


您遇到的问题是:

  • 缩放和拖动自定义内的图像ClipPath
  • 在两个自定义之间拖动图像ClipPath

我建议的解决方案是使用拖动手柄来交换图像

!!!剧透:它不起作用(还)!

要使用 custom 实现这种拖放操作ClipPath,我们需要HitTestBehavior.deferToChildon的支持DragTarget

好消息是……它已经在 Fluttermaster频道中可用了![参考]

因此,如果您可以稍等片刻,它会在 中发布stable,这是我的解决方案:

在此处输入图像描述

主要思想是将可缩放图像设置为DragTargets,并为每个图像设置一个拖动句柄Draggable

我添加了一层状态管理以在交换图像时保持缩放级别和偏移量。

我还改进了可缩放功能,以确保图像始终覆盖整个ClipPath.

完整的源代码(250 行)

import 'dart:math' show min, max;

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

part '66474773.drag.freezed.dart';

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Flutter Demo',
        home: HomePage(),
      ),
    ),
  );
}

class HomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final images = useProvider(imagesProvider.state);
    final _width = MediaQuery.of(context).size.shortestSide * .8;

    void swapImages() => context.read(imagesProvider).swap();

    return Scaffold(
      backgroundColor: Colors.black87,
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Container(
            height: _width,
            width: _width,
            child: Stack(
              children: [
                DragTarget<VerticalDirection>(
                  hitTestBehavior: HitTestBehavior.deferToChild,
                  onWillAccept: (direction) =>
                      direction == VerticalDirection.up,
                  onAccept: (_) => swapImages(),
                  builder: (_, __, ___) => _Zoomable(
                    key: GlobalKey(),
                    width: _width,
                    pathFn: topPathFn,
                    imageId: 0,
                  ),
                ),
                DragTarget<VerticalDirection>(
                  hitTestBehavior: HitTestBehavior.deferToChild,
                  onWillAccept: (direction) =>
                      direction == VerticalDirection.down,
                  onAccept: (_) => swapImages(),
                  builder: (_, __, ___) => _Zoomable(
                    key: GlobalKey(),
                    width: _width,
                    pathFn: bottomPathFn,
                    imageId: 1,
                  ),
                ),
                Positioned.fill(
                  child: Align(
                    alignment: Alignment.topLeft,
                    child: _DragHandle(
                      direction: VerticalDirection.down,
                      imgAssetPath: images[0].assetPath,
                    ),
                  ),
                ),
                Positioned.fill(
                  child: Align(
                    alignment: Alignment.bottomRight,
                    child: _DragHandle(
                      direction: VerticalDirection.up,
                      imgAssetPath: images[1].assetPath,
                    ),
                  ),
                ),
              ],
            )),
      ),
    );
  }
}

class _DragHandle extends StatelessWidget {
  final VerticalDirection direction;
  final String imgAssetPath;

  const _DragHandle({Key key, this.direction, this.imgAssetPath})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Draggable<VerticalDirection>(
      data: direction,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.grey.shade200,
          border: Border.all(color: Colors.grey.shade700),
        ),
        child: Icon(Icons.open_with),
      ),
      childWhenDragging: Container(),
      feedback: Image.asset(imgAssetPath, width: 80),
    );
  }
}

class _Zoomable extends HookWidget {
  final double width;
  final Path Function(Size) pathFn;
  final int imageId;

  const _Zoomable({
    Key key,
    this.width,
    this.pathFn,
    this.imageId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final image =
        useProvider(imagesProvider.state.select((state) => state[imageId]));
    final _startingFocalPoint = useState(Offset.zero);
    final _previousOffset = useState<Offset>(null);
    final _offset = useState(image.offset);
    final _previousZoom = useState<double>(null);
    final _zoom = useState(image.zoom);
    return CustomPaint(
      painter: MyPainter(pathFn: pathFn),
      child: GestureDetector(
        onTap: () {}, // onScaleUpdate not triggered if onTap is not defined
        onScaleStart: (details) {
          _startingFocalPoint.value = details.focalPoint;
          _previousOffset.value = _offset.value;
          _previousZoom.value = _zoom.value;
        },
        onScaleUpdate: (details) {
          _zoom.value = max(1, _previousZoom.value * details.scale);
          final newOffset = details.focalPoint -
              (_startingFocalPoint.value - _previousOffset.value) *
                  details.scale;
          _offset.value = Offset(
            min(0, max(-width * (_zoom.value - 1), newOffset.dx)),
            min(0, max(-width * (_zoom.value - 1), newOffset.dy)),
          );
        },
        onScaleEnd: (_) => context.read(imagesProvider).update(
            imageId, image.copyWith(zoom: _zoom.value, offset: _offset.value)),
        child: ClipPath(
          clipper: MyClipper(pathFn: pathFn),
          child: Transform(
            transform: Matrix4.identity()
              ..translate(_offset.value.dx, _offset.value.dy)
              ..scale(_zoom.value),
            child: Image.asset(
              image.assetPath,
              width: width,
              height: width,
              fit: BoxFit.fill,
            ),
          ),
        ),
      ),
    );
  }
}

Path bottomPathFn(Size size) => Path()
  ..moveTo(size.width, 0)
  ..lineTo(0, size.height)
  ..lineTo(size.height, size.height)
  ..close();

Path topPathFn(Size size) => Path()
  ..moveTo(size.width, 0)
  ..lineTo(0, size.height)
  ..lineTo(0, 0)
  ..close();

class MyClipper extends CustomClipper<Path> {
  final Path Function(Size) pathFn;

  MyClipper({this.pathFn});

  @override
  getClip(Size size) => pathFn(size);

  @override
  bool shouldReclip(CustomClipper oldClipper) {
    return false;
  }
}

class MyPainter extends CustomPainter {
  final Path Function(Size) pathFn;

  Path _path;

  MyPainter({this.pathFn});

  @override
  void paint(Canvas canvas, Size size) {
    _path = pathFn(size);
    final paint = Paint()
      ..color = Colors.white
      ..strokeWidth = 4.0
      ..style = PaintingStyle.stroke;
    canvas.drawPath(_path, paint);
  }

  @override
  bool hitTest(Offset position) {
    return _path?.contains(position);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

final imagesProvider =
    StateNotifierProvider<ImagesNotifier>((ref) => ImagesNotifier([
          ZoomedImage(assetPath: 'images/abstract.jpg'),
          ZoomedImage(assetPath: 'images/abstract2.jpg'),
        ]));

class ImagesNotifier extends StateNotifier<List<ZoomedImage>> {
  ImagesNotifier(List<ZoomedImage> state) : super(state);

  void swap() {
    state = state.reversed.toList();
  }

  void update(int id, ZoomedImage updatedImage) {
    state = [...state]..[id] = updatedImage;
  }
}

@freezed
abstract class ZoomedImage with _$ZoomedImage {
  const factory ZoomedImage({
    String assetPath,
    @Default(1.0) double zoom,
    @Default(Offset.zero) Offset offset,
  }) = _ZoomedImage;
}

推荐阅读