Flutter 使用 GestureDetectors 制作图像也可拖动
Flutter make image with GestureDetectors also Draggable
我的目标是有一个 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
也不工作。
实现此目标的最佳方法是什么?我在这方面找不到任何信息...如果您需要更多详细信息,请告诉我!
您遇到的问题是:
- 在自定义中缩放和拖动图像
ClipPath
- 在两个自定义之间拖动图像
ClipPath
我建议的解决方案是使用拖动手柄来交换图像
!!!剧透:它(还)不起作用!!!
要使用自定义 ClipPath
实现此拖放操作,我们需要 DragTarget
上的 HitTestBehavior.deferToChild
支持。
好消息是...它已经在 Flutter master
频道中可用了! [ref]
因此,如果您可以稍等片刻,等待它在 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;
}
我的目标是有一个 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
也不工作。
实现此目标的最佳方法是什么?我在这方面找不到任何信息...如果您需要更多详细信息,请告诉我!
您遇到的问题是:
- 在自定义中缩放和拖动图像
ClipPath
- 在两个自定义之间拖动图像
ClipPath
我建议的解决方案是使用拖动手柄来交换图像
!!!剧透:它(还)不起作用!!!
要使用自定义 ClipPath
实现此拖放操作,我们需要 DragTarget
上的 HitTestBehavior.deferToChild
支持。
好消息是...它已经在 Flutter master
频道中可用了! [ref]
因此,如果您可以稍等片刻,等待它在 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;
}