Как получить высоту виджета?

Я не понимаю, как LayoutBuilder используется для получения высоты виджета.

Мне нужно отобразить список виджетов и получить их высоту, чтобы я мог вычислить некоторые специальные эффекты прокрутки. Я разрабатываю пакет, а другие разработчики предоставляют виджет (я их не контролирую). Я прочитал, что LayoutBuilder можно использовать для получения высоты.

В очень простом случае я попытался обернуть виджет в LayoutBuilder.builder и поместить его в стек, но я всегда получаю minHeight 0.0 и maxHeight INFINITY. Я неправильно использую LayoutBuilder?

EDIT. Кажется, что LayoutBuilder - это не выход. Я нашел CustomSingleChildLayout, что является почти решением.

Я расширил этот делегат, и мне удалось получить высоту виджета в методе getPositionForChild(Size size, Size childSize). НО, первый метод, который называется Size getSize(BoxConstraints constraints) и как ограничения, я получаю 0 до INFINITY, потому что я кладу эти CustomSingleChildLayouts в ListView.

Моя проблема в том, что SingleChildLayoutDelegate getSize работает так, как будто нужно вернуть высоту представления. В тот момент я не знаю высоты ребенка. Я могу только вернуть constraints.smallest(что равно 0, высота равно 0) или constraints.biggest, что бесконечно, и сбой приложения.

В документах он даже говорит:

... но размер родителя не может зависеть от размера дочернего элемента.

И это странное ограничение.

Ответ 1

Чтобы получить размер/положение виджета на экране, вы можете использовать GlobalKey, чтобы получить его BuildContext, чтобы затем найти RenderBox этого конкретного виджета, который будет содержать его глобальную позицию и размер визуализации.

Только одна вещь, чтобы быть осторожным: этот контекст может не существовать, если виджет не отображается. Что может вызвать проблемы с ListView, поскольку виджеты отображаются только в том случае, если они потенциально видимы.

Другая проблема заключается в том, что вы не можете получить виджет RenderBox во время вызова build, так как виджет еще не обработан.


Но мне нужен размер при сборке! Что я могу сделать?

Есть один классный виджет, который может помочь: Overlay и его OverlayEntry. Они используются для отображения виджетов поверх всего остального (аналогично стеку).

Но самое классное то, что они находятся в другом потоке build; они построены после обычных виджетов.

Это имеет один очень крутой смысл: OverlayEntry может иметь размер, который зависит от виджетов реального дерева виджетов.


Хорошо. Но разве OverlayEntry не требуется перестраивать вручную?

Да, они делают. Но есть еще одна вещь, о которой следует знать: ScrollController, переданный в Scrollable, похож на прослушиваемый текст AnimationController.

Это означает, что вы можете объединить AnimatedBuilder с ScrollController, это будет иметь прекрасный эффект, чтобы автоматически перестроить ваш виджет на свитке. идеально подходит для этой ситуации, верно?


Объединяем все в пример:

В следующем примере вы увидите наложение, которое следует за виджетом внутри ListView и имеет ту же высоту.

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final controller = ScrollController();
  OverlayEntry sticky;
  GlobalKey stickyKey = GlobalKey();

  @override
  void initState() {
    if (sticky != null) {
      sticky.remove();
    }
    sticky = OverlayEntry(
      opaque: false,
      // lambda created to help working with hot-reload
      builder: (context) => stickyBuilder(context),
    );

    // not possible inside initState
    SchedulerBinding.instance.addPostFrameCallback((_) {
      Overlay.of(context).insert(sticky);
    });
    super.initState();
  }

  @override
  void dispose() {
    // remove possible overlays on dispose as they would be visible even after [Navigator.push]
    sticky.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: controller,
        itemBuilder: (context, index) {
          if (index == 6) {
            return Container(
              key: stickyKey,
              height: 100.0,
              color: Colors.green,
              child: const Text("I'm fat"),
            );
          }
          return ListTile(
            title: Text(
              'Hello $index',
              style: const TextStyle(color: Colors.white),
            ),
          );
        },
      ),
    );
  }

  Widget stickyBuilder(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (_,Widget child) {
        final keyContext = stickyKey.currentContext;
        if (keyContext != null) {
          // widget is visible
          final box = keyContext.findRenderObject() as RenderBox;
          final pos = box.localToGlobal(Offset.zero);
          return Positioned(
            top: pos.dy + box.size.height,
            left: 50.0,
            right: 50.0,
            height: box.size.height,
            child: Material(
              child: Container(
                alignment: Alignment.center,
                color: Colors.purple,
                child: const Text("^ Nah I think you're okay"),
              ),
            ),
          );
        }
        return Container();
      },
    );
  }
}

Ответ 2

Если я правильно понимаю, вы хотите измерить размерность некоторых произвольных виджетов, и вы можете обернуть эти виджеты другим виджетом. В этом случае метод этого ответа должен работать для вас.

В основном решение состоит в том, чтобы связать обратный вызов в жизненном цикле виджета, который будет вызываться после рендеринга первого кадра, откуда вы можете получить доступ к context.size. Подвох в том, что вы должны обернуть виджет, который вы хотите измерить, в виджет с сохранением состояния. И, если вам абсолютно необходим размер в пределах build(), вы можете получить к нему доступ только во втором рендере (он доступен только после первого рендера).