fix: trying to avoid conflict first with tap and pin on plotting screen
This commit is contained in:
@@ -530,6 +530,46 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
..rotateZ((provider.cropRotation) * (math.pi / 180)),
|
||||
alignment: Alignment.center,
|
||||
child: GestureDetector(
|
||||
// AJOUT D'IMPACT : géré ici, sur le GestureDetector parent, par un
|
||||
// simple tap. Un seul détecteur de tap -> plus de couche concurrente
|
||||
// avec l'InteractiveViewer, donc le pinch/zoom redevient fiable.
|
||||
//
|
||||
// Flutter ne déclenche onTapUp que si le doigt n'a pas bougé au-delà
|
||||
// du seuil (touch slop) : un déplacement part en pan via
|
||||
// l'InteractiveViewer, un toucher bref pose un impact.
|
||||
onTapUp: (TapUpDetails details) {
|
||||
// Pendant le déplacement d'un impact, on n'ajoute rien.
|
||||
if (_movingShotId != null) return;
|
||||
|
||||
final RenderBox? box =
|
||||
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
|
||||
final localOffset = box.globalToLocal(details.globalPosition);
|
||||
final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
|
||||
final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0);
|
||||
|
||||
// Si le tap tombe sur un impact existant, on l'ouvre plutôt que
|
||||
// d'en empiler un nouveau par-dessus.
|
||||
Shot? hitShot;
|
||||
double minDistance = double.infinity;
|
||||
const double hitTolerance = 0.04;
|
||||
for (final shot in provider.shots) {
|
||||
final dx = shot.x - relX;
|
||||
final dy = shot.y - relY;
|
||||
final distance = math.sqrt(dx * dx + dy * dy);
|
||||
if (distance < minDistance && distance < hitTolerance) {
|
||||
minDistance = distance;
|
||||
hitShot = shot;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitShot != null) {
|
||||
_showShotDetails(context, provider, hitShot);
|
||||
} else {
|
||||
provider.addShot(relX, relY);
|
||||
}
|
||||
},
|
||||
onDoubleTapDown: (TapDownDetails details) {
|
||||
final RenderBox? box =
|
||||
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
@@ -632,7 +672,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
zoomScale: _currentZoomScale,
|
||||
onShotTapped: (shot) =>
|
||||
_showShotDetails(context, provider, shot),
|
||||
onAddShot: (relX, relY) => provider.addShot(relX, relY),
|
||||
// onAddShot retiré : l'ajout est géré par le GestureDetector
|
||||
// parent (onTapUp) pour ne pas concurrencer le pinch/zoom.
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/// Overlay visuel de la cible.
|
||||
///
|
||||
/// Dessine les anneaux de la cible, les impacts détectés, le cercle de groupement
|
||||
/// et les impacts de référence. Gère les interactions tactiles pour l'ajout
|
||||
/// d'impacts et la sélection d'impacts existants.
|
||||
/// et les impacts de référence. Gère uniquement la SÉLECTION d'impacts existants
|
||||
/// (tap sur un impact). L'AJOUT d'un impact est délégué à l'écran parent pour
|
||||
/// éviter tout conflit de gestes avec le zoom/pan de l'InteractiveViewer.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -48,18 +49,17 @@ class TargetOverlay extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapUp: (details) {
|
||||
if (onAddShot != null) {
|
||||
// Utiliser les constraints pour un calcul précis
|
||||
final relX = details.localPosition.dx / constraints.maxWidth;
|
||||
final relY = details.localPosition.dy / constraints.maxHeight;
|
||||
onAddShot!(relX, relY);
|
||||
}
|
||||
},
|
||||
// IMPORTANT : plus de GestureDetector global ici.
|
||||
// L'ancien GestureDetector (onTapUp couvrant toute la surface, en
|
||||
// HitTestBehavior.translucent) volait les pointeurs au pinch de
|
||||
// l'InteractiveViewer parent et rendait le zoom capricieux.
|
||||
//
|
||||
// Désormais :
|
||||
// - L'AJOUT d'impact est géré par le GestureDetector parent (analysis_screen).
|
||||
// - Seule la SÉLECTION d'un impact existant est gérée ici, via des petites
|
||||
// zones de tap localisées (deferToChild) placées sur chaque impact.
|
||||
return IgnorePointer(
|
||||
ignoring: false,
|
||||
child: CustomPaint(
|
||||
painter: _TargetOverlayPainter(
|
||||
shots: shots,
|
||||
@@ -76,48 +76,42 @@ class TargetOverlay extends StatelessWidget {
|
||||
zoomScale: zoomScale,
|
||||
showRings: showRings,
|
||||
),
|
||||
child: Stack(
|
||||
children: shots.map((shot) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, innerConstraints) {
|
||||
final x = shot.x * innerConstraints.maxWidth;
|
||||
final y = shot.y * innerConstraints.maxHeight;
|
||||
// Zone de tap qui s'adapte au zoom (taille fixe à l'écran)
|
||||
builder: (context, constraints) {
|
||||
return Stack(
|
||||
children: shots.map((shot) {
|
||||
final x = shot.x * constraints.maxWidth;
|
||||
final y = shot.y * constraints.maxHeight;
|
||||
// Zone de tap qui reste constante à l'écran malgré le zoom.
|
||||
final tapSize = 30 / zoomScale;
|
||||
final halfTapSize = tapSize / 2;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
return Positioned(
|
||||
left: x - halfTapSize,
|
||||
top: y - halfTapSize,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
// deferToChild : ne capte le toucher QUE sur la zone du
|
||||
// Container (un cercle opaque au hit-test), pas ailleurs.
|
||||
// Le reste de la surface reste donc disponible pour le
|
||||
// pinch/pan de l'InteractiveViewer.
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTap: () => onShotTapped?.call(shot),
|
||||
child: Container(
|
||||
width: tapSize,
|
||||
height: tapSize,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
// Opaque pour le hit-test (couleur transparente visuellement
|
||||
// mais non nulle), pour que le tap soit bien capté ici.
|
||||
color: Color(0x01000000),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -300,9 +294,9 @@ class _TargetOverlayPainter extends CustomPainter {
|
||||
..strokeWidth = strokeWidth;
|
||||
canvas.drawCircle(Offset(x, y), outerRadius, outlinePaint);
|
||||
|
||||
// Draw impact marker — TRANSPARENCE 50% pour voir l'impact réel derrière
|
||||
// Draw impact marker — TRANSPARENCE 30% pour voir l'impact réel derrière
|
||||
final impactPaint = Paint()
|
||||
..color = AppTheme.impactColor.withValues(alpha: 0.2)
|
||||
..color = AppTheme.impactColor.withValues(alpha: 0.3)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(Offset(x, y), innerRadius, impactPaint);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user