Задний план
Попытка настроить простой редактор изображений, позволяющий пользователям масштабировать и перемещать изображения с помощью жестов.
Масштабирование и перемещение хорошо работают с помощью GestureDetector, Transform.scale и Transform.rotate.
Проблема
После масштабирования пользователь по-прежнему может масштабировать уже масштабированные изображения.
Но: GestureDetector не меняет область для выполнения тестов на попадание.
Проблема: Пользователь может использовать оригинальный хитбокс только для манипуляций с изображениями. Невозможно масштабировать изображение, используя жест сжатия двумя пальцами на расширенной внешней форме.
Картинки
Первое изображение демонстрирует базовую настройку.
Второе изображение демонстрирует результат использования жеста. Он показывает маленький, неизменный внутренний хитбокс. А также полученную масштабированную и повернутую форму.
Заполненное поле — это хитбокс. Внешний прямоугольник показывает масштабированное изображение.
Желаемое поведение
Использование жеста сжатия двумя пальцами на масштабированной внешней форме должно позволять дальнейшие манипуляции с объектом.
Вместо этого внутренний хитбокс можно использовать отдельно. Но пользователь рассчитывает использовать масштабированную внешнюю форму для дальнейшего масштабирования и перемещения объекта.
Код
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: GestureTest(),
);
}
}
class DrawContainer {
Color color;
Offset offset;
double scale;
double angle;
late double baseScaleFactor;
DrawContainer(this.color, this.offset, this.scale, this.angle) {
baseScaleFactor = scale;
}
onScaleStart() => baseScaleFactor = scale;
onScaleUpdate(double scaleNew) =>
scale = (baseScaleFactor * scaleNew).clamp(0.5, 5);
}
class GestureTest extends StatefulWidget {
const GestureTest({Key? key}) : super(key: key);
@override
// ignore: library_private_types_in_public_api
_GestureTestState createState() => _GestureTestState();
}
class _GestureTestState extends State<GestureTest> {
bool doRedraw = false;
final List<DrawContainer> containers = [
DrawContainer(Colors.red, const Offset(50, 50), 1.0, 0.0),
DrawContainer(Colors.yellow, const Offset(100, 100), 1.0, 0.0),
DrawContainer(Colors.green, const Offset(150, 150), 1.0, 0.0),
];
void onGestureStart(DrawContainer e) => e.onScaleStart();
onGestureUpdate(DrawContainer e, ScaleUpdateDetails d) {
e.offset = e.offset + d.focalPointDelta;
if (d.rotation != 0.0) e.angle = d.rotation;
if (d.scale != 1.0) e.onScaleUpdate(d.scale);
setState(() => doRedraw = !doRedraw); // redraw
}
void rebuildAllChildren(BuildContext context) {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(context as Element).visitChildren(rebuild);
}
@override
Widget build(BuildContext context) {
rebuildAllChildren(context);
return SafeArea(
child: Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
doRedraw ? const SizedBox.shrink() : const SizedBox.shrink(),
...containers.map((e) {
return Positioned(
top: e.offset.dy,
left: e.offset.dx,
child: Container(
color: e.color,
child: GestureDetector(
onScaleStart: (details) {
if (details.pointerCount == 2) {
onGestureStart(e);
}
},
onScaleUpdate: (details) => onGestureUpdate(e, details),
child: Transform.rotate(
angle: e.angle,
child: Transform.scale(
scale: e.scale,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: e.color)),
width: 100,
height: 100),
// Text(e.label, style: const TextStyle(fontSize: 40)),
))),
// ),
));
}).toList(),
],
),
));
}
}
@pskink Спасибо, попытался увеличить родительский элемент ad hoc, но не знаю, как применить это в моем сценарии. Завтра попробую еще раз.





Ниже приведен общий рабочий пример с размером хитбокса, соответствующим масштабируемому размеру виджета.
Базовая структура выглядит следующим образом:
SizedBox (infinite size) # may not be needed
- Stack
- GestureDetector for each Widget
- Stack
- Positioned, Transform
- Widget
import 'package:flutter/material.dart';
// -------------------------------------------------------------------
// THE ITEM TO BE DRAWN
// -------------------------------------------------------------------
class DrawContainer {
Color color;
Offset offset;
double width;
double height;
double scale;
double angle;
late double _baseScaleFactor;
late double _baseAngleFactor;
DrawContainer(this.color, this.offset, this.width, this.height, this.scale,
this.angle) {
onScaleStart();
}
onScaleStart() {
_baseScaleFactor = scale;
_baseAngleFactor = angle;
}
onScaleUpdate(double scaleNew) =>
scale = (_baseScaleFactor * scaleNew).clamp(0.5, 5);
onRotateUpdate(double angleNew) => angle = _baseAngleFactor + angleNew;
}
// -------------------------------------------------------------------
// APP
// -------------------------------------------------------------------
void main() {
runApp(const MaterialApp(home: GestureTest()));
}
class GestureTest extends StatefulWidget {
const GestureTest({Key? key}) : super(key: key);
@override
// ignore: library_private_types_in_public_api
_GestureTestState createState() => _GestureTestState();
}
// -------------------------------------------------------------------
// APP STATE
// -------------------------------------------------------------------
class _GestureTestState extends State<GestureTest> {
final List<DrawContainer> containers = [
DrawContainer(Colors.red, const Offset(50, 50), 100, 100, 1.0, 0.0),
DrawContainer(Colors.yellow, const Offset(100, 100), 200, 100, 1.0, 0.0),
DrawContainer(Colors.green, const Offset(150, 150), 50, 100, 1.0, 0.0),
];
void onGestureStart(DrawContainer e) => e.onScaleStart();
onGestureUpdate(DrawContainer e, ScaleUpdateDetails d) {
e.offset = e.offset + d.focalPointDelta;
if (d.rotation != 0.0) e.onRotateUpdate(d.rotation);
if (d.scale != 1.0) e.onScaleUpdate(d.scale);
setState(() {}); // redraw
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: SizedBox(
height: double.infinity,
width: double.infinity,
child: Stack(
children: [
...containers.map((e) {
return GestureDetector(
onScaleStart: (details) {
// detect two fingers to reset internal factors
if (details.pointerCount == 2) {
onGestureStart(e);
}
},
onScaleUpdate: (details) => onGestureUpdate(e, details),
child: DrawWidget(e));
}).toList(),
],
),
),
));
}
}
// -------------------------------------------------------------------
// POSITION, ROTATE AND SCALE THE WIDGET
// -------------------------------------------------------------------
class DrawWidget extends StatelessWidget {
final DrawContainer e;
const DrawWidget(this.e, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
left: e.offset.dx,
top: e.offset.dy,
child: Transform.rotate(
angle: e.angle,
child: Transform.scale(
scale: e.scale,
child: Container(
height: e.width,
width: e.height,
color: e.color,
),
),
),
),
],
);
}
}
Этот тестовый пример был полезен: https://stackoverflow.com/a/68360447/12098106