Flutter使带有GestureDetectors的图像也可拖动 [英] Flutter make image with GestureDetectors also Draggable

查看:76
本文介绍了Flutter使带有GestureDetectors的图像也可拖动的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的目标是拥有一个可以缩放的 image CustomClipperImage 内部移动,它也应该是 Draggable

现在,我可以在其 Clip 中对其进行 scale 缩放,如下所示:

主要思想是将可缩放图像设置为 DragTargets ,对于每个图像,将拖动手柄设置为 Draggable .

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

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

完整的源代码(250行)

  import'dart:math'show min,max;导入'package:flutter/material.dart';导入'package:flutter_hooks/flutter_hooks.dart';导入'package:freezed_annotation/freezed_annotation.dart';导入'package:hooks_riverpod/hooks_riverpod.dart';部分'66474773.drag.freezed.dart';void main(){runApp(ProviderScope(子:MaterialApp(debugShowCheckedModeBanner:否,标题:"Flutter演示",主页:HomePage(),),),);}类HomePage扩展了HookWidget {@override窗口小部件build(BuildContext context){最终图像= useProvider(imagesProvider.state);final _width = MediaQuery.of(context).size.shortestSide * .8;void swapImages()=>context.read(imagesProvider).swap();返回脚手架(backgroundColor:Colors.black87,身体:填充(填充:const EdgeInsets.all(24.0),子代:集装箱(高度宽度,宽度:_width,子代:Stack(孩子们: [DragTarget< VerticalDirection>(hitTestBehavior:HitTestBehavior.deferToChild,onWillAccept :(方向)=>方向== VerticalDirection.up,onAccept:(_)=>swapImages(),生成器:(_,__,___)=>_Zoomable(密钥:GlobalKey(),宽度:_width,pathFn:topPathFn,imageId:0,),),DragTarget< VerticalDirection>(hitTestBehavior:HitTestBehavior.deferToChild,onWillAccept :(方向)=>方向== VerticalDirection.down,onAccept:(_)=>swapImages(),生成器:(_,__,___)=>_Zoomable(密钥:GlobalKey(),宽度:_width,pathFn:bottomPathFn,imageId:1),),Positioned.fill(子级:Align(对齐方式:Alignment.topLeft,子:_DragHandle(方向:VerticalDirection.down,imgAssetPath:图片[0] .assetPath,),),),Positioned.fill(子级:Align(对齐方式:Alignment.bottomRight,子:_DragHandle(方向:VerticalDirection.up,imgAssetPath:图片[1] .assetPath,),),),],)),),);}}_DragHandle类扩展了StatelessWidget {最终的VerticalDirection方向;最终字符串imgAssetPath;const _DragHandle({Key key,this.direction,this.imgAssetPath}):super(key:键);@override窗口小部件build(BuildContext context){返回Draggable< VerticalDirection>(数据:方向,子代:集装箱(装饰:BoxDecoration(颜色:Colors.grey.shade200,边框:Border.all(颜色:Colors.grey.shade700),),子级:Icon(Icons.open_with),),childWhenDragging:Container(),反馈:Image.asset(imgAssetPath,宽度:80),);}}_Zoomable类扩展了HookWidget {最终双倍宽度;最终路径功能(大小)pathFn;最终的int imageId;const _Zoomable({关键这个宽度this.pathFn,this.imageId,}):super(key:key);@override窗口小部件build(BuildContext context){最终图片=useProvider(imagesProvider.state.select((state)=> state [imageId]));final _startingFocalPoint = useState(Offset.zero);final _previousOffset = useState< Offset>(null);最终_offset = useState(image.offset);final _previousZoom = useState< double>(null);final _zoom = useState(image.zoom);返回CustomPaint(画家:MyPainter(pathFn:pathFn),子代:GestureDetector(onTap:(){},//如果未定义onTap,则不会触发onScaleUpdateonScaleStart :(详细信息){_startingFocalPoint.value = details.focalPoint;_previousOffset.value = _offset.value;_previousZoom.value = _zoom.value;},onScaleUpdate :(详细信息){_zoom.value = max(1,_previousZoom.value * details.scale);最后的newOffset = details.focalPoint-(_startingFocalPoint.value-_previousOffset.value)*细节比例;_offset.value =偏移量(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)),子代:ClipPath(快船:MyClipper(pathFn:pathFn),子级:Transform(转换:Matrix4.identity()..translate(_offset.value.dx,_offset.value.dy)..scale(_zoom.value),子:Image.asset(image.assetPath,宽度:宽度,高度宽度,适合:BoxFit.fill,),),),),);}}路径bottomPathFn(Size size)=>小路()..moveTo(size.width,0)..lineTo(0,size.height)..lineTo(size.height,size.height)..关闭();路径topPathFn(Size size)=>小路()..moveTo(size.width,0)..lineTo(0,size.height)..lineTo(0,0)..关闭();MyClipper类扩展了CustomClipper< Path>{最终路径功能(大小)pathFn;MyClipper({this.pathFn});@overridegetClip(Size size)=>pathFn(size);@overridebool shouldReclip(CustomClipper oldClipper){返回false;}}MyPainter类扩展了CustomPainter {最终路径功能(大小)pathFn;路径_path;MyPainter({this.pathFn});@override无效油漆(帆布,尺寸){_path = pathFn(size);最终油漆= Paint()..color =颜色.白色..strokeWidth = 4.0..style = PaintingStyle.stroke;canvas.drawPath(_path,paint);}@overridebool hitTest(Offset position){返回_path?.contains(position);}@overridebool shouldRepaint(covariant CustomPainter oldDelegate)=>错误的;}最终imagesProvider =StateNotifierProvider< ImagesNotifier>((ref)=> ImagesNotifier([ZoomedImage(assetPath:'images/abstract.jpg'),ZoomedImage(assetPath:'images/abstract2.jpg'),]));class ImagesNotifier扩展StateNotifier< List< List&ZoomedImage>>{ImagesNotifier(List< ZoomedImage> state):super(state);无效swap(){状态= state.reversed.toList();}void update(int id,ZoomedImage UpdatedImage){状态= [...状态] .. [id] = UpdatedImage;}}@freezed具有_ $ ZoomedImage的抽象类ZoomedImage {const factory ZoomedImage({字符串assetPath,@Default(1.0)双重缩放,@Default(Offset.zero)偏移量偏移量,})= _ZoomedImage;} 

My goal is to have an image that I can zoom & move around inside a CustomClipperImage and it should also be Draggable!

Right now I can scale the image in its Clip and this looks like this:

Screenvideo

This is the code for it:

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

But I can not make it Draggable... I tried wrapping the whole Container or also just the Image.asset inside Draggable but when doing this, scaling stops working and Draggable is not working either.

What is the best way to achieve this? I couldn't find anything on this... Let me know if you need more details!

解决方案

The problem you have is a conflict between:

  • zooming and dragging the image inside the custom ClipPath
  • dragging the images between two custom ClipPath

The solution I propose is to use drag handles to swap the images

!!! SPOILER : It does not work (yet) !!!

To implement this drag-n-drop with custom ClipPath, we need the support of HitTestBehavior.deferToChild on DragTarget.

The good news is... It's already available in Flutter master channel! [ref]

So, if you can wait a bit for it to be released in stable, here is my solution:

The main idea is to have the zoomable images as DragTargets and for each image a drag handle as Draggable.

I added a layer of State Management to keep the zoom level and offset when swapping the images.

I also improved the zoomable feature to ensure that the image always covers the full ClipPath.

Full source code (250 lines)

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;
}

这篇关于Flutter使带有GestureDetectors的图像也可拖动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆