Flutter: GestureHandler и Transform.scale приводят к небольшому хит-боксу

Задний план

Попытка настроить простой редактор изображений, позволяющий пользователям масштабировать и перемещать изображения с помощью жестов.

Масштабирование и перемещение хорошо работают с помощью 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(),
        ],
      ),
    ));
  }
}

github.com/flutter/flutter/issues/27587
pskink 17.10.2022 21:33

@pskink Спасибо, попытался увеличить родительский элемент ad hoc, но не знаю, как применить это в моем сценарии. Завтра попробую еще раз.

Dabbel 17.10.2022 23:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
129
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ниже приведен общий рабочий пример с размером хитбокса, соответствующим масштабируемому размеру виджета.

Базовая структура выглядит следующим образом:

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

Другие вопросы по теме