1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-keyboard-rs.md 15 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Дело было таким, что недавно компания Flutter выпустила стабильную версию 1.17, и в соответствии с "традициями" началась работа по обновлению производственного проекта до версии 1.12.13+hotfix.9. После завершения процесса адаптации внезапная ошибка заставила меня задуматься.

image

Как показано на приведённой выше图为所展示的,可以看到在键盘B页面打开后,返回至上一页面A时键盘已收回,但原先键盘所在区域在A页面变为了空白,并且A页面的内容也因键盘弹出而进行了重新调整大小。

1. Scaffold

По поводу этой проблемы, первым делом приходит в голову свойство resizeToAvoidBottomInset компонента Scaffold.

В Flutter по умолчанию значение свойства resizeToAvoidBottomInset компонента Scaffold равно true. Когда значение этого свойства равно true, внутри Scaffold медиа запрос mediaQuery.viewInsets.bottom включается в расчет размера BoxConstraints. То есть при появлении клавиатуры внутренний нижний край экрана корректируется для учета высоты клавиатуры.

Однако проблема возникает на странице A, где клавиатура уже скрыта, и значение mediaQuery.viewInsets.bottom должно быть обновлено до нуля. Почему же интерфейс не обновляется соответственно?

2. MediaQuery

Тогда можно предположить, что проблема может быть связана с компонентом MediaQuery.

Согласно исходному коду мы знаем, что MediaQuery — это InheritedWidget, который передает соответствующий объект MediaQueryData, содержащий различные данные устройства, такие как размер, devicePixelRatio, textScaleFactor, viewPadding и viewInsets.

А что такое viewInsets? Официальное объяснение следующее:

"Это область, которая может быть отображена системой, обычно связанная с клавиатурой устройства. При появлении клавиатуры значение viewInsets.bottom соответствует верхней части клавиатуры."

Таким образом, указанный выше сбой кажется результатом некорректного обновления значения viewInsets.bottom компонента Scaffold после того, как клавиатура была скрыта.

3. Window

Чтобы понять, как viewInsets компонента MediaQuery устанавливается, следует сначала узнать, как это происходит.После анализа исходного кода можно сделать вывод, что MediaQueryData из MediaQuery получена из WidgetsBinding.instance.window, и по умолчанию она задается в _MediaQueryFromWindow в MaterialApp:

  @override
  void didChangeMetrics() {
    setState(() {
      // Свойства окна были изменены. Мы используем их в нашей функции сборки,
      // поэтому нам требуется setState(), но мы ничего не кэшируем локально.
    });
  }

  @override
  Widget build(BuildContext context) {
    return MediaQuery(
      data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
      child: widget.child,
    );
  }

Исходный код демонстрирует, что MediaQueryData берётся из объекта Window, а также регистрируется обратный вызов didChangeMetrics для WidgetsBindingObserver. Это означает, что при изменении window будет вызван метод setState, чтобы обновить данные в MediaQuery. ```Исходный код демонстрирует, что MediaQueryData берётся из объекта `Window`, а также регистрируется обратный вызов `didChangeMetrics` для `WidgetsBindingObserver`. Это значит, что при изменении `window` будет вызван метод `setState`, чтобы обновить данные в `MediaQuery`.

В методе MediaQueryData.fromWindow значение viewInsets вычисляется путём деления значений window.viewInsets и window.devicePixelRatio.

viewInsets = EdgeInsets.fromWindowPadding(window.viewInsets, window.devicePixelRatio);

Но откуда берутся значения в объекте Window?

На самом деле значения в объекте Window берутся из движка Flutter. Когда клавиатура появляется, движок Flutter использует метод _updateWindowMetrics, чтобы обновить данные в объекте Window и выполнить методы window.onMetricsChanged и window._onMetricsChangedZone.Обратный вызов onMetricsChanged в конечном итоге вызывает метод handleMetricsChanged, который выполняет scheduleForcedFrame(), чтобы обновить интерфейс, и вызывает observer.didChangeMetrics();, чтобы сообщить о необходимости обновления данных MediaQueryData внутри MaterialApp.

@pragma('vm:entry-point')
// ignore: unused_element
void _updateWindowMetrics(
  double devicePixelRatio,
  double width,
  double height,
  double depth,
  double viewPaddingTop,
  double viewPaddingRight,
  double viewPaddingBottom,
  double viewPaddingLeft,
  double viewInsetTop,
  double viewInsetRight,
  double viewInsetBottom,
  double viewInsetLeft,
  double systemGestureInsetTop,
  double systemGestureInsetRight,
  double systemGestureInsetBottom,
  double systemGestureInsetLeft,
) {
  window
    .._devicePixelRatio = devicePixelRatio
    .._physicalSize = Size(width, height)
    .._physicalDepth = depth
    .._viewPadding = WindowPadding._(
        top: viewPaddingTop,
        right: viewPaddingRight,
        bottom: viewPaddingBottom,
        left: viewPaddingLeft)
    .._viewInsets = WindowPadding._(
        top: viewInsetTop,
        right: viewInsetRight,
        bottom: viewInsetBottom,
        left: viewInsetLeft)
    .._padding = WindowPadding._(
        top: math.max(0.0, viewPaddingTop - viewInsetTop),
        right: math.max(0.0, viewPaddingRight - viewInsetRight),
        bottom: math.max(0.0, viewPaddingBottom - viewInsetBottom),
        left: math.max(0.0, viewPaddingLeft - viewInsetLeft))
    .._systemGestureInsets = WindowPadding._(
        top: math.max(0.0, systemGestureInsetTop),
        right: math.max(0.0, systemGestureInsetRight),
        bottom: math.max(0.0, systemGestureInsetBottom),
        left: math.max(0.0, systemGestureInsetLeft));
  _invoke(window.onMetricsChanged, window._onMetricsChangedZone);
}

Как можно заметить, когда клавиатура появляется и исчезает, Engine обновляет данные окна Window, а Window запускает обновление интерфейса и одновременно обновляет MediaQueryData в MaterialApp.Иллюстрация

4. Маршрут

Учитывая эту ситуацию, проблема с пустыми областями, вызванной клавиатурой, должна была бы не возникнуть. Однако проблема может заключаться в том, что MediaQueryData в Scaffold не обновляется.

В этот момент мне пришло в голову, что ранее, чтобы заблокировать изменение размера шрифта страницы вместе со сжатием системы, я использовал MediaQueryData.fromWindow для создания копии MediaQuery на уровне маршрутов. Возможно, проблема связана именно с этим:

Navigator.of(context).push(new CupertinoPageRoute(builder: (context) {
   return MediaQuery(
      data: MediaQueryData.fromWindow(WidgetsBinding.instance.window)
                          .copyWith(textScaleFactor: 1),
      child: Page2(),
   );
}));

Однако это также выглядит неправильным, так как проблема возникает при переходе с экрана B, где есть клавиатура, обратно на экран A, где её нет. В этом случае экран A уже открыт, поэтому WidgetsBinding.instance.window должен быть правильным. А не должен ли метод builder маршрута CupertinoPageRoute для экрана A снова выполняться при открытии экрана B с клавиатурой?

После отладки я был удивлен тем фактом, что программа после открытия экрана B с клавиатурой действительно триггерит повторное выполнение метода builder маршрута CupertinoPageRoute для экрана A.Первое, что приходит на ум при возможности обновления между экранами — это глобальная система управления состоянием, поскольку приложение требует глобального изменения темы, многоязычности и совместного использования информации пользователя. Обычно такие данные передаются и управляются через систему управления состоянием на уровне всего приложения.Учитывая сложность исходного проекта, я создал простой тестовый демон и внедрил в него простую систему управления состоянием ScopedModel. После открытия экрана B с клавиатурой и выполнения notifyListeners() с некоторым временем задержки, я обнаружил, что действительно возникает аналогичная проблема.

return ScopedModel(
  model: t,
  child: ScopedModelDescendant<TestModel>(
    builder: (context, child, model) {
      return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: 'Flutter Demo Home Page'),
      );
    },
  ),
);

5. Навигатор

Здесь возникают вопросы: почему обновление MaterialApp приводит к тому, что PageRoute вызывает метод builder заново?

Это связано с логикой работы Navigator, который является StatefulWidget. Когда MaterialApp обновляется, в методе didUpdateWidget класса NavigatorState вызывается метод changedExternalState() для всех маршрутов в истории навигатора.

@Override
void didUpdateWidget(Navigator oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.observers != widget.observers) {
    for (NavigatorObserver observer in oldWidget.observers)
      observer._navigator = null;
    for (NavigatorObserver observer in widget.observers) {
      assert(observer.navigator == null);
      observer._navigator = this;
    }
  }
  for (Route<dynamic> route in _history)
    route.changedExternalState();
}

Вызов метода changedExternalState приводит к выполнению _forceRebuildPage, который очищает поле _page маршрута. Это значит, что при следующем вызове метода build для маршрута будет снова вызван метод builder.```dart @Override void changedExternalState() { super.changedExternalState(); if (_scopeKey.currentState != null) _scopeKey.currentState._forceRebuildPage(); }

...

void _forceRebuildPage() { setState(() { _page = null; }); }


Итак, вернемся к первоначальному вопросу: **эта проблема произошла из-за некорректного использования `MediaQueryData.fromWindow(WidgetsBinding.instance.window)`. После открытия страницы с клавиатурой было вызвано обновление `MaterialApp`, что привело к повторному вызову метода `builder` для `PageRoute`. В результате `Scaffold` без клавиатуры использовал значение `viewInsets.bottom` для открытого с клавиатурой экрана.**

**Для решения этой проблемы достаточно заменить `MediaQueryData.fromWindow` на `MediaQuery.of(context)`. При отсутствии контекста или необходимости использовать `MediaQueryData.fromWindow` напрямую, следует использовать `WidgetsBindingObserver.didChangeMetrics` для обновления данных.**

```dart
Navigator.of(context).push(new CupertinoPageRoute(builder: (context) {
  return MediaQuery(
    data: MediaQuery.of(context)
        .copyWith(textScaleFactor: 1),
    child: Page2(),
  );
}));

Наконец, стоит отметить, что хотя эта проблема и не сложна, она помогает выявить множество часто игнорируемых аспектов программирования. Поэтому подробное описание поможет вам решать больше задач в повседневной разработке.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master