概述

#

大多數平臺上的觸控板手勢現在會發送 PointerPanZoom 序列,並可以觸發平移、拖動和縮放 GestureRecognizer 回撥。

背景

#

在 Flutter 3.3.0 版本之前的 Flutter 桌面版中,滾動使用的是 PointerScrollEvent 訊息來表示離散的滾動距離。這個系統對於滑鼠滾輪效果很好,但對於觸控板滾動來說並不理想。觸控板滾動應具有慣性,這不僅取決於滾動距離,還取決於手指從觸控板上抬起的時間。此外,觸控板捏合縮放也無法表示。

引入了三個新的 PointerEventPointerPanZoomStartEventPointerPanZoomUpdateEventPointerPanZoomEndEvent。相關的 GestureRecognizer 已更新,以註冊對觸控板手勢序列的興趣,並將在檢測到觸控板上的兩個或多個手指移動時,發出 onDragonPan 和/或 onScale 回撥。

這意味著,僅為觸控互動設計的程式碼可能會在觸控板互動時觸發,而為處理所有桌面滾動設計的程式碼現在可能僅在滑鼠滾動時觸發,而在觸控板滾動時不再觸發。

變更說明

#

Flutter 引擎已在所有可能的平臺上更新,以識別觸控板手勢,並將它們作為 PointerPanZoom 事件傳送到框架,而不是作為 PointerScrollSignal 事件。PointerScrollSignal 事件仍將用於表示滑鼠滾輪上的滾動。

根據平臺和具體的觸控板型號,如果平臺 API 提供的 Flutter 引擎資料不足,則可能不會使用新系統。這包括 Windows(觸控板手勢支援取決於觸控板驅動程式)和 Web 平臺(瀏覽器 API 提供的資料不足,觸控板滾動仍需使用舊的 PointerScrollSignal 系統)。

開發者應準備好接收這兩種型別的事件,並確保他們的應用程式或軟體包以適當的方式處理它們。

Listener 現在有三個新的回撥:onPointerPanZoomStartonPointerPanZoomUpdateonPointerPanZoomEnd,可用於觀察觸控板滾動和縮放事件。

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('mouse scrolled ${event.scrollDelta}');
        }
      },
      onPointerPanZoomStart: (PointerPanZoomStartEvent event) {
        debugPrint('trackpad scroll started');
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scrolled ${event.panDelta}');
      },
      onPointerPanZoomEnd: (PointerPanZoomEndEvent event) {
        debugPrint('trackpad scroll ended');
      },
      child: Container()
    );
  }
}

PointerPanZoomUpdateEvent 包含一個 pan 欄位,用於表示當前手勢的累積平移;一個 panDelta 欄位,用於表示自上次事件以來平移的差異;一個 scale 欄位,用於表示當前手勢的累積縮放;以及一個 rotation 欄位,用於表示累積旋轉(以弧度為單位)的手勢。

GestureRecognizer 現在都有方法可以處理來自單個連續觸控板手勢的所有觸控板事件。在 GestureRecognizer 上呼叫 addPointerPanZoom 方法並傳入 PointerPanZoomStartEvent,將使識別器註冊其對該觸控板互動的興趣,並解決可能響應該手勢的多個 GestureRecognizer 之間的衝突。

以下示例展示瞭如何正確使用 ListenerGestureRecognizer 來響應觸控板互動。

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

使用 GestureDetector 時,這是自動完成的,因此像下面的示例這樣的程式碼將響應觸控和平移觸控板手勢來發出其手勢更新回撥。

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

遷移指南

#

遷移步驟取決於您希望應用程式中的每個手勢互動都能透過觸控板使用,還是僅限於觸控和滑鼠使用。

適用於觸控板使用的手勢互動

#

使用 GestureDetector

#

無需更改,GestureDetector 會自動處理觸控板手勢事件,並在識別後觸發回撥。

使用 GestureRecognizerListener

#

確保 onPointerPanZoomStartListener 傳遞給每個識別器。必須呼叫 `GestureRecognizer` 的 addPointerPanZoom 方法,才能使其顯示興趣並開始跟蹤每個觸控板手勢。

遷移前的程式碼

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      child: Container()
    );
  }
}

遷移後的程式碼

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

使用原始 Listener

#

以下使用 PointerScrollSignal 的程式碼將不再被所有桌面滾動呼叫。應捕獲 PointerPanZoomUpdate 事件以接收觸控板手勢資料。

遷移前的程式碼

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      }
      child: Container()
    );
  }
}

遷移後的程式碼

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scroll event');
      }
      child: Container()
    );
  }
}

請注意:以這種方式使用原始 Listener 可能會與其他手勢互動衝突,因為它不參與手勢歧義處理區域。

不適用於觸控板使用的手勢互動

#

使用 GestureDetector

#

如果使用 Flutter 3.3.0,可以使用 RawGestureDetector 代替 GestureDetector,以確保 GestureDetector 建立的每個 GestureRecognizersupportedDevices 都設定為排除 PointerDeviceKind.trackpad。從 3.4.0 版本開始,GestureDetector 上直接有一個 supportedDevices 引數。

遷移前的程式碼

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

遷移後的程式碼(Flutter 3.3.0)

dart
// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

遷移後的程式碼:(Flutter 3.4.0)

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      supportedDevices: {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,
        PointerDeviceKind.invertedStylus,
        // Do not include PointerDeviceKind.trackpad
      },
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

使用 RawGestureRecognizer

#

明確確保 supportedDevices 不包含 PointerDeviceKind.trackpad

遷移前的程式碼

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

遷移後的程式碼

dart
// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

使用 GestureRecognizerListener

#

升級到 Flutter 3.3.0 後,行為不會發生改變,因為必須在每個 GestureRecognizer 上呼叫 addPointerPanZoom 才能使其跟蹤手勢。當滾動觸控板時,以下程式碼將不會收到平移手勢回撥。

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      // recognizer.addPointerPanZoom is not called
      child: Container()
    );
  }
}

時間線

#

已釋出版本:3.3.0-0.0.pre
穩定版釋出:3.3.0

參考資料

#

API 文件

設計文件

相關問題

相關 PR