image - 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
也不工作。
实现这一目标的最佳方法是什么?我在这方面找不到任何东西...如果您需要更多详细信息,请告诉我!
解决方案
您遇到的问题是:
- 缩放和拖动自定义内的图像
ClipPath
- 在两个自定义之间拖动图像
ClipPath
我建议的解决方案是使用拖动手柄来交换图像
!!!剧透:它不起作用(还)!
要使用 custom 实现这种拖放操作ClipPath
,我们需要HitTestBehavior.deferToChild
on的支持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;
}
推荐阅读
- audio - 如何让 Safari 播放本地音频资源?
- python - 如何将文件中的字符串数据转换为公式的整数形式
- r - 如何用另一个data.frame列的值填充data.frame列的每n行?
- c++ - 如何将向量的元素与数组进行比较?
- javascript - 带有 JSCValue* 对象的 jsc_value_object_invoke_method() 不起作用
- php - PHP水平合并数组
- c# - 显示带有线程睡眠的随机数生成器的过程
- html - 如何在 Ace 文本编辑器中打开文件?
- mysql - 复杂搜索 Laravel 雄辩
- android - 如何避免 Android RecyclerView 中的 IndexOutOfBoundsException?