362 lines
9.7 KiB
Markdown
362 lines
9.7 KiB
Markdown
|
|
# ピンチジェスチャー不安定性の修正 v1.0.12+24
|
|||
|
|
|
|||
|
|
## 📅 修正日時
|
|||
|
|
2026年2月7日
|
|||
|
|
|
|||
|
|
## 🐛 報告された問題
|
|||
|
|
|
|||
|
|
### ユーザーレポート
|
|||
|
|
> マップのピンチインピンチアウトは基本的に問題なく動作するけど、初めて真横にピンチアウトしようとすると反応せず、何回か触って拡大する時と初回から拡大する時もあり、挙動が安定していないかもしれません
|
|||
|
|
|
|||
|
|
### 症状
|
|||
|
|
- **初回の真横ピンチアウト**: 反応しない
|
|||
|
|
- **何回か触ると**: 拡大する(時もある)
|
|||
|
|
- **挙動**: 不安定
|
|||
|
|
- **特に問題**: 横方向のピンチジェスチャー
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 根本原因の分析
|
|||
|
|
|
|||
|
|
### 問題のあったコード(v1.0.12+23)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// brewery_map_screen.dart:138-140
|
|||
|
|
minScale: fitScale * 0.95, // ❌ 動的な値に依存
|
|||
|
|
maxScale: fitScale * 6.0, // ❌ 動的な値に依存
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### なぜ不安定だったのか
|
|||
|
|
|
|||
|
|
#### 1. **動的スケール値の問題**
|
|||
|
|
```dart
|
|||
|
|
final fitScale = (availableWidth / mapWidth) * 0.95;
|
|||
|
|
|
|||
|
|
// 例: iPhone 14 Pro (393pt width)
|
|||
|
|
fitScale = (393 / 600) * 0.95 = 0.62175
|
|||
|
|
|
|||
|
|
// InteractiveViewerの設定
|
|||
|
|
minScale: 0.62175 * 0.95 = 0.590 // 初期スケールより小さい
|
|||
|
|
maxScale: 0.62175 * 6.0 = 3.73 // 初期スケールの6倍
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. **初期変換行列との矛盾**
|
|||
|
|
```dart
|
|||
|
|
// 初期変換行列(Line 123-125)
|
|||
|
|
final matrix = Matrix4.identity()
|
|||
|
|
..translateByVector3(Vector3(xOffset, 10.0, 0.0))
|
|||
|
|
..scaleByVector3(Vector3(fitScale, fitScale, 1.0)); // fitScaleで拡大済み
|
|||
|
|
|
|||
|
|
// しかし、minScale/maxScale も fitScale に依存している
|
|||
|
|
// → ジェスチャー認識が混乱
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. **Flutter InteractiveViewerの仕様**
|
|||
|
|
- `minScale` / `maxScale` は**現在の変換行列を基準**とする
|
|||
|
|
- 初期変換行列で `fitScale` を適用済み
|
|||
|
|
- さらに `minScale` / `maxScale` も `fitScale` に依存
|
|||
|
|
- → スケール値が二重に適用され、ジェスチャー認識が不安定に
|
|||
|
|
|
|||
|
|
#### 4. **真横ピンチアウトで反応しない理由**
|
|||
|
|
```
|
|||
|
|
真横のピンチアウト
|
|||
|
|
↓
|
|||
|
|
InteractiveViewerが水平方向の変化を検出
|
|||
|
|
↓
|
|||
|
|
スケール計算: 現在のスケール × 変化量
|
|||
|
|
↓
|
|||
|
|
minScale (fitScale * 0.95) の制約チェック
|
|||
|
|
↓
|
|||
|
|
計算結果が不安定 → ジェスチャー無視
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 修正内容
|
|||
|
|
|
|||
|
|
### 修正後のコード(v1.0.12+24)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// brewery_map_screen.dart:137-143
|
|||
|
|
// v1.0.12+24: Fixed pinch gesture instability by using fixed scale values
|
|||
|
|
// The initial transformation already handles fitScale, so we use 1.0 as base
|
|||
|
|
minScale: 0.5, // Allow zoom out to 50% of initial size
|
|||
|
|
maxScale: 6.0, // Allow zoom in to 600% of initial size
|
|||
|
|
constrained: false,
|
|||
|
|
// Enable all pan axes for smooth gesture recognition
|
|||
|
|
panAxis: PanAxis.free,
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 修正の要点
|
|||
|
|
|
|||
|
|
#### 1. **固定スケール値の使用**
|
|||
|
|
```dart
|
|||
|
|
minScale: 0.5, // ✅ 固定値(fitScaleに依存しない)
|
|||
|
|
maxScale: 6.0, // ✅ 固定値(fitScaleに依存しない)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**理由**:
|
|||
|
|
- 初期変換行列で `fitScale` を適用済み
|
|||
|
|
- `minScale` / `maxScale` は「初期状態からの倍率」として機能
|
|||
|
|
- 動的な値に依存しないため、ジェスチャー認識が安定
|
|||
|
|
|
|||
|
|
#### 2. **panAxis: PanAxis.free の追加**
|
|||
|
|
```dart
|
|||
|
|
panAxis: PanAxis.free, // ✅ すべての方向のパンを許可
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**理由**:
|
|||
|
|
- デフォルトでは一部の方向のパンが制限される場合がある
|
|||
|
|
- `PanAxis.free` で**すべての方向のジェスチャーを有効化**
|
|||
|
|
- 真横のピンチアウトも確実に認識される
|
|||
|
|
|
|||
|
|
#### 3. **コメントで意図を明確化**
|
|||
|
|
```dart
|
|||
|
|
// v1.0.12+24: Fixed pinch gesture instability by using fixed scale values
|
|||
|
|
// The initial transformation already handles fitScale, so we use 1.0 as base
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧪 期待される動作
|
|||
|
|
|
|||
|
|
### 修正前(v1.0.12+23)
|
|||
|
|
```
|
|||
|
|
初回の真横ピンチアウト
|
|||
|
|
↓
|
|||
|
|
スケール計算が不安定
|
|||
|
|
↓
|
|||
|
|
❌ 反応しない(または遅延)
|
|||
|
|
|
|||
|
|
何回か触る
|
|||
|
|
↓
|
|||
|
|
ジェスチャー履歴が蓄積
|
|||
|
|
↓
|
|||
|
|
△ 時々反応する
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 修正後(v1.0.12+24)
|
|||
|
|
```
|
|||
|
|
初回の真横ピンチアウト
|
|||
|
|
↓
|
|||
|
|
固定スケール値で計算
|
|||
|
|
↓
|
|||
|
|
✅ 即座に反応
|
|||
|
|
|
|||
|
|
どの方向のピンチでも
|
|||
|
|
↓
|
|||
|
|
PanAxis.free で認識
|
|||
|
|
↓
|
|||
|
|
✅ 安定して動作
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 技術的評価
|
|||
|
|
|
|||
|
|
### スケール範囲の比較
|
|||
|
|
|
|||
|
|
#### Before (v1.0.12+23)
|
|||
|
|
```
|
|||
|
|
fitScale = 0.62 (iPhone 14 Pro)
|
|||
|
|
|
|||
|
|
minScale = 0.62 * 0.95 = 0.59 (初期状態より縮小可能)
|
|||
|
|
maxScale = 0.62 * 6.0 = 3.72 (初期状態の6倍)
|
|||
|
|
|
|||
|
|
問題:
|
|||
|
|
- 初期変換行列でfitScale適用済み
|
|||
|
|
- さらにminScale/maxScaleもfitScaleに依存
|
|||
|
|
- スケール値が二重適用される
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### After (v1.0.12+24)
|
|||
|
|
```
|
|||
|
|
初期変換行列: fitScale = 0.62 (固定)
|
|||
|
|
|
|||
|
|
minScale = 0.5 (初期状態の50%まで縮小可能)
|
|||
|
|
maxScale = 6.0 (初期状態の600%まで拡大可能)
|
|||
|
|
|
|||
|
|
改善:
|
|||
|
|
- 初期変換行列でfitScale適用
|
|||
|
|
- minScale/maxScaleは固定値
|
|||
|
|
- スケール値の二重適用を回避
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ジェスチャー認識の改善
|
|||
|
|
|
|||
|
|
#### Before
|
|||
|
|
```
|
|||
|
|
真横ピンチアウト
|
|||
|
|
↓
|
|||
|
|
動的minScale/maxScaleで計算
|
|||
|
|
↓
|
|||
|
|
計算結果が不安定
|
|||
|
|
↓
|
|||
|
|
❌ ジェスチャー無視(または遅延)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### After
|
|||
|
|
```
|
|||
|
|
真横ピンチアウト
|
|||
|
|
↓
|
|||
|
|
固定minScale/maxScaleで計算
|
|||
|
|
↓
|
|||
|
|
PanAxis.freeで全方向許可
|
|||
|
|
↓
|
|||
|
|
✅ 安定したジェスチャー認識
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧪 実機テスト項目(必須)
|
|||
|
|
|
|||
|
|
### テスト1: 真横ピンチアウト(修正の主目的)
|
|||
|
|
1. マップ画面を開く
|
|||
|
|
2. **初回から真横にピンチアウト**(2本指を水平に広げる)
|
|||
|
|
3. **期待結果**: 即座に拡大される ✅
|
|||
|
|
|
|||
|
|
### テスト2: すべての方向のピンチイン/アウト
|
|||
|
|
1. **真横にピンチイン/アウト**(水平方向)
|
|||
|
|
2. **真縦にピンチイン/アウト**(垂直方向)
|
|||
|
|
3. **斜めにピンチイン/アウト**(ななめ方向)
|
|||
|
|
4. **期待結果**: すべての方向で安定して動作 ✅
|
|||
|
|
|
|||
|
|
### テスト3: 最小/最大スケールの確認
|
|||
|
|
1. ピンチインで縮小 → 初期サイズの50%まで縮小可能
|
|||
|
|
2. ピンチアウトで拡大 → 初期サイズの600%まで拡大可能
|
|||
|
|
3. **期待結果**: 適切な範囲で拡大縮小される ✅
|
|||
|
|
|
|||
|
|
### テスト4: パンとピンチの組み合わせ
|
|||
|
|
1. ピンチアウトで拡大
|
|||
|
|
2. 1本指でドラッグ移動
|
|||
|
|
3. ピンチインで縮小
|
|||
|
|
4. **期待結果**: スムーズに動作、ジェスチャー干渉なし ✅
|
|||
|
|
|
|||
|
|
### テスト5: リセットボタンの動作
|
|||
|
|
1. 拡大・移動後、リセットボタンをタップ
|
|||
|
|
2. **期待結果**: 初期位置・サイズに戻る ✅
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📈 品質指標
|
|||
|
|
|
|||
|
|
### flutter analyze結果
|
|||
|
|
```
|
|||
|
|
Before (v1.0.12+23): 34 issues
|
|||
|
|
After (v1.0.12+24): 34 issues
|
|||
|
|
変化: なし ✅ (デグレなし)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### コンパイル結果
|
|||
|
|
- ✅ エラー: 0件
|
|||
|
|
- ✅ 警告: 0件
|
|||
|
|
- ✅ ビルド成功
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 修正の影響範囲
|
|||
|
|
|
|||
|
|
### 変更されたファイル
|
|||
|
|
1. **[lib/screens/placeholders/brewery_map_screen.dart:137-143](lib/screens/placeholders/brewery_map_screen.dart#L137-L143)**
|
|||
|
|
|
|||
|
|
### 変更されていない機能
|
|||
|
|
- ✅ 初期表示位置・サイズ
|
|||
|
|
- ✅ リセットボタンの動作
|
|||
|
|
- ✅ 都道府県タップ機能
|
|||
|
|
- ✅ ドラッグ移動
|
|||
|
|
- ✅ その他すべての機能
|
|||
|
|
|
|||
|
|
### リスク評価
|
|||
|
|
- **リスクレベル**: 🟢 低
|
|||
|
|
- **理由**: InteractiveViewerのパラメータ変更のみ
|
|||
|
|
- **デグレ可能性**: 極めて低い
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💡 技術的解説(開発者向け)
|
|||
|
|
|
|||
|
|
### なぜ固定値が正しいのか
|
|||
|
|
|
|||
|
|
#### InteractiveViewerの仕様
|
|||
|
|
```dart
|
|||
|
|
// InteractiveViewerは内部で以下のように動作:
|
|||
|
|
final currentScale = transformationController.value.getMaxScaleOnAxis();
|
|||
|
|
final newScale = currentScale * gestureScaleDelta;
|
|||
|
|
|
|||
|
|
if (newScale < minScale) {
|
|||
|
|
// スケール変更を制限
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 問題のあった設定
|
|||
|
|
```dart
|
|||
|
|
// 初期変換行列
|
|||
|
|
transformationController.value = Matrix4.identity()
|
|||
|
|
..scale(fitScale, fitScale, 1.0); // 例: fitScale = 0.62
|
|||
|
|
|
|||
|
|
// InteractiveViewerの設定
|
|||
|
|
minScale: fitScale * 0.95 // 0.59
|
|||
|
|
maxScale: fitScale * 6.0 // 3.72
|
|||
|
|
|
|||
|
|
// ジェスチャー時の計算
|
|||
|
|
currentScale = 0.62 // 初期変換行列のスケール
|
|||
|
|
newScale = 0.62 * 1.5 // ピンチアウトで1.5倍
|
|||
|
|
// → newScale = 0.93
|
|||
|
|
|
|||
|
|
// minScale/maxScaleのチェック
|
|||
|
|
if (0.93 < 0.59) { ... } // false
|
|||
|
|
if (0.93 > 3.72) { ... } // false
|
|||
|
|
// → 許可される
|
|||
|
|
|
|||
|
|
// しかし、fitScaleが動的に変わる場合、計算が不安定に
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 正しい設定
|
|||
|
|
```dart
|
|||
|
|
// 初期変換行列
|
|||
|
|
transformationController.value = Matrix4.identity()
|
|||
|
|
..scale(fitScale, fitScale, 1.0); // 例: fitScale = 0.62
|
|||
|
|
|
|||
|
|
// InteractiveViewerの設定(固定値)
|
|||
|
|
minScale: 0.5
|
|||
|
|
maxScale: 6.0
|
|||
|
|
|
|||
|
|
// ジェスチャー時の計算
|
|||
|
|
currentScale = 0.62 // 初期変換行列のスケール
|
|||
|
|
newScale = 0.62 * 1.5 // ピンチアウトで1.5倍
|
|||
|
|
// → newScale = 0.93
|
|||
|
|
|
|||
|
|
// minScale/maxScaleのチェック(固定値で安定)
|
|||
|
|
if (0.93 < 0.5) { ... } // false
|
|||
|
|
if (0.93 > 6.0) { ... } // false
|
|||
|
|
// → 許可される(安定)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎉 まとめ
|
|||
|
|
|
|||
|
|
### 修正内容
|
|||
|
|
1. ✅ `minScale` を `fitScale * 0.95` → `0.5` に変更(固定値)
|
|||
|
|
2. ✅ `maxScale` を `fitScale * 6.0` → `6.0` に変更(固定値)
|
|||
|
|
3. ✅ `panAxis: PanAxis.free` を追加(すべての方向のジェスチャーを許可)
|
|||
|
|
|
|||
|
|
### 期待される改善
|
|||
|
|
- ✅ 真横ピンチアウトが初回から反応
|
|||
|
|
- ✅ すべての方向のピンチイン/アウトが安定
|
|||
|
|
- ✅ ジェスチャー認識の遅延がなくなる
|
|||
|
|
- ✅ UXの大幅な向上
|
|||
|
|
|
|||
|
|
### 次のアクション
|
|||
|
|
1. 📱 **実機テスト**: 上記のテスト項目を実行
|
|||
|
|
2. ✅ **配布**: 問題なければv1.0.12+24としてリリース
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**作成者**: Claude (Sonnet 4.5)
|
|||
|
|
**修正日時**: 2026年2月7日
|
|||
|
|
**対象バージョン**: v1.0.12+24 (未リリース)
|
|||
|
|
**修正種別**: UX改善(ピンチジェスチャー安定化)
|
|||
|
|
**リスクレベル**: 🟢 低
|