Flutter使带有GestureDetectors的图像也可拖动 [英] Flutter make image with GestureDetectors also Draggable
问题描述
我的目标是拥有一个可以缩放的 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:
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屋!