Làm thế nào để có được chiều cao của một Widget?


82

Tôi không hiểu cách LayoutBuilderđược sử dụng để lấy chiều cao của một Widget.

Tôi cần hiển thị danh sách các Widget và lấy chiều cao của chúng để có thể tính toán một số hiệu ứng cuộn đặc biệt. Tôi đang phát triển một gói và các nhà phát triển khác cung cấp widget (tôi không kiểm soát chúng). Tôi đọc rằng LayoutBuilder có thể được sử dụng để lấy chiều cao.

Trong trường hợp rất đơn giản, tôi đã cố gắng bọc Widget trong LayoutBuilder.builder và đặt nó vào Stack, nhưng tôi luôn nhận được minHeight 0.0, và maxHeight INFINITY. Tôi có đang lạm dụng LayoutBuilder không?

CHỈNH SỬA : Có vẻ như LayoutBuilder là một lựa chọn không tồi. Tôi đã tìm thấy CustomSingleChildLayout gần như là một giải pháp.

Tôi đã mở rộng đại biểu đó và tôi có thể lấy chiều cao của tiện ích con trong getPositionForChild(Size size, Size childSize)phương thức. NHƯNG, phương thức đầu tiên được gọi là Size getSize(BoxConstraints constraints)và dưới dạng các ràng buộc, tôi nhận được 0 cho INFINITY vì tôi đang đặt các CustomSingleChildLayouts này trong một ListView.

Vấn đề của tôi là SingleChildLayoutDelegate getSizehoạt động giống như nó cần trả lại chiều cao của một chế độ xem. Tôi không biết chiều cao của một đứa trẻ tại thời điểm đó. Tôi chỉ có thể trả về constraints.smallest (là 0, chiều cao là 0) hoặc binds.biggest là vô cùng và làm ứng dụng bị treo.

Trong tài liệu, nó thậm chí còn nói:

... nhưng kích thước của cha mẹ không thể phụ thuộc vào kích thước của con cái.

Và đó là một hạn chế kỳ lạ.


1
LayoutBuilder sẽ cung cấp cho bạn các ràng buộc hộp của cha mẹ. Nếu bạn muốn kích thước của đứa trẻ, bạn cần một chiến lược khác. Một ví dụ mà tôi có thể chỉ ra là tiện ích Wrap, nó bố trí dựa trên kích thước của nó là con trong lớp RenderWrap được liên kết. Tuy nhiên, điều này xảy ra trong quá trình bố trí, không phải build ().
Jonah Williams

@JonahWilliams Hừm. Tôi không biết Wrap có thể giúp gì cho tôi vì nó là tiện ích được thiết kế để bố trí trẻ em xung quanh (hoạt động giống như lưới flexbox trên web). Tôi có một tiện ích con mà tôi cần tìm chiều cao của nó. Vui lòng xem phần chỉnh sửa trong câu hỏi. Tôi gần như đã giải quyết được vấn đề với CustomSingleChildLayout nhưng gặp khó khăn về giới hạn của nó.
Gudin,

Bạn có thể giải thích những gì bạn muốn cụ thể hơn? Có nhiều giải pháp. Nhưng mỗi loại có các trường hợp sử dụng khác nhau.
Rémi Rousselet,

Chắc chắn rồi. Tôi đang phát triển một gói. Người dùng / Nhà phát triển cung cấp một Widget cho lớp của tôi. Chúng ta đang nói về bất kỳ widget nào ở đây, từ những widget new Text("hello")phức tạp hơn. Tôi đặt các widget này vào ListView và tôi cần chiều cao của chúng để tính toán một số hiệu ứng cuộn. Tôi đồng ý với việc lấy chiều cao tại thời điểm bố trí, giống như những gì SingleChildLayoutDelegate đang làm.
Gudin,

Bạn có nghĩa là gì bởi "Hiệu ứng cuộn"? Bạn có một ví dụ?
Rémi Rousselet

Câu trả lời:


150

Để có kích thước / vị trí của một tiện ích con trên màn hình, bạn có thể sử dụng GlobalKeyđể lấy nó BuildContextđể sau đó tìm RenderBoxtiện ích cụ thể đó, sẽ chứa vị trí chung và kích thước hiển thị của nó.

Chỉ cần cẩn thận một điều: Ngữ cảnh đó có thể không tồn tại nếu tiện ích con không được hiển thị. Điều này có thể gây ra sự cố ListViewvì các tiện ích con chỉ được hiển thị nếu chúng có khả năng hiển thị.

Một vấn đề khác là bạn không thể nhận tiện ích con RenderBoxtrong khi buildgọi vì tiện ích con chưa được hiển thị.


Nhưng tôi cần kích thước trong quá trình xây dựng! Tôi có thể làm gì?

Có một tiện ích tuyệt vời có thể giúp: Overlayvà của nó OverlayEntry. Chúng được sử dụng để hiển thị các widget trên đầu mọi thứ khác (tương tự như ngăn xếp).

Nhưng điều thú vị nhất là chúng ở một builddòng chảy khác ; chúng được xây dựng sau các vật dụng thông thường.

Điều đó có một ngụ ý cực kỳ thú vị: OverlayEntrycó thể có kích thước phụ thuộc vào các widget của cây widget thực tế.


Được chứ. Nhưng OverlayEntry không yêu cầu được xây dựng lại theo cách thủ công?

Có, họ có. Nhưng có một điều khác cần lưu ý:, được ScrollControllerchuyển cho a Scrollable, là một tương tự có thể nghe được AnimationController.

Có nghĩa là bạn có thể kết hợp một AnimatedBuildervới một ScrollController, nó sẽ có hiệu ứng đáng yêu để xây dựng lại tiện ích con của bạn tự động trên một cuộn. Hoàn hảo cho tình huống này, phải không?


Kết hợp mọi thứ thành một ví dụ:

Trong ví dụ sau, bạn sẽ thấy một lớp phủ theo sau một tiện ích con bên trong ListViewvà có cùng chiều cao.

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(
      builder: (context) => stickyBuilder(context),
    );

    SchedulerBinding.instance.addPostFrameCallback((_) {
      Overlay.of(context).insert(sticky);
    });

    super.initState();
  }

  @override
  void dispose() {
    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();
      },
    );
  }
}

Ghi chú :

Khi điều hướng đến một màn hình khác, cuộc gọi sau nếu không dính sẽ vẫn hiển thị.

sticky.remove();

5
Ok, cuối cùng đã có thời gian để kiểm tra nó. Vì vậy, tôi thực sự chỉ cần phần đầu tiên. Tôi không biết mình có thể sử dụng quyền truy cập vào context.height với GlobalKey. Câu trả lời chính xác.
Gudin

1
làm thế nào để nhập chất kết dính lịch trình?
siddhesh

1
tôi đã thử nhập 'gói: flay / Scheduler.dart.'; nhưng tôi đã nhận mục tiêu lỗi uri không tồn tại @ Rémi-Rousselet
siddhesh

@ rémi-rousselet làm cách nào để làm cho điều này hoạt động khi tôi có một tiện ích con phía sau ListView có chiều cao mà tôi muốn kiểm soát theo chiều cao của ListView?
RoyalGriffin 11/09/19

2
Remi, Cảm ơn vì giải pháp thông minh và lời giải thích tuyệt vời. Tôi có một câu hỏi. Điều gì sẽ xảy ra nếu chúng ta muốn có thể biết Rectbất kỳ ListItem nào của một ListView.builderkhi chúng được nhấn. Nó có phải là một sự quá mức cần thiết để đặt GlobalKey listItemKey = GlobalKey();cho mọi listItem không? Giả sử tôi có hơn 10000 mục. Có cách nào thông minh để quản lý điều này mà không gặp vấn đề về hiệu suất / bộ nhớ không?
aytunch

39

Đây là (tôi nghĩ) cách đơn giản nhất để làm điều này.

Sao chép-dán phần sau vào dự án của bạn.

CẬP NHẬT: sử dụng RenderProxyBoxkết quả trong việc triển khai đúng hơn một chút, vì nó được gọi trong mỗi lần xây dựng lại con và con của nó, điều này không phải lúc nào cũng đúng với phương thức build () cấp cao nhất.

LƯU Ý: Đây không phải là một cách hiệu quả để thực hiện việc này, như đã được Hixie chỉ ra ở đây . Nhưng nó là dễ nhất.

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

typedef void OnWidgetSizeChange(Size size);

class MeasureSizeRenderObject extends RenderProxyBox {
  Size oldSize;
  final OnWidgetSizeChange onChange;

  MeasureSizeRenderObject(this.onChange);

  @override
  void performLayout() {
    super.performLayout();

    Size newSize = child.size;
    if (oldSize == newSize) return;

    oldSize = newSize;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      onChange(newSize);
    });
  }
}

class MeasureSize extends SingleChildRenderObjectWidget {
  final OnWidgetSizeChange onChange;

  const MeasureSize({
    Key key,
    @required this.onChange,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return MeasureSizeRenderObject(onChange);
  }
}

Sau đó, chỉ cần quấn tiện ích có kích thước bạn muốn đo MeasureSize.


var myChildSize = Size.zero;

Widget build(BuildContext context) {
  return ...( 
    child: MeasureSize(
      onChange: (size) {
        setState(() {
          myChildSize = size;
        });
      },
      child: ...
    ),
  );
}

Vì vậy, có, kích thước của cha mẹ không thể phụ thuộc vào kích thước của con nếu bạn đủ cố gắng.


Giai thoại cá nhân - Điều này rất hữu ích để hạn chế kích thước của các vật dụng như Alignchiếm một lượng không gian vô lý.


Những gì bạn có ở đây là tuyệt vời. Không có lời giải thích điên rồ .. chỉ cần mã hoạt động. Cảm ơn!
Danny Daglas

Giải pháp tuyệt vời anh em. làm cho cái này như một gói quán rượu.
Harsh Bhikadia

Giải pháp rất tốt của nó cho nhiều vấn đề về chiều cao và chiều rộng, Cảm ơn
alireza daryani

Điều này hoạt động, nhưng nó khó sử dụng ở một số nơi. Ví dụ: trong a PreferredSizeWidget, preferredSizechỉ được gọi một lần, vì vậy bạn không thể đặt chiều cao một cách dễ dàng.
user2233706

Ý tưởng tuyệt vời, nhưng điều này sẽ không hoạt động trên TextFormField
Iworb

6

Nếu tôi hiểu đúng, bạn muốn đo kích thước của một số tiện ích con tùy ý và bạn có thể bọc các tiện ích đó bằng một tiện ích con khác. Trong trường hợp đó, phương pháp trong câu trả lời này sẽ phù hợp với bạn.

Về cơ bản, giải pháp là ràng buộc một lệnh gọi lại trong vòng đời tiện ích con, lệnh gọi này sẽ được gọi sau khi khung đầu tiên được hiển thị, từ đó bạn có thể truy cập context.size. Điểm bắt buộc là bạn phải bọc widget bạn muốn đo trong một widget trạng thái. Và, nếu bạn thực sự cần kích thước bên trong build()thì bạn chỉ có thể truy cập nó trong lần hiển thị thứ hai (nó chỉ khả dụng sau lần hiển thị đầu tiên).


1
Cảm ơn con trỏ, hãy xem giải pháp hoàn chỉnh của tôi - stackoverflow.com/a/60868972/7061265
Dev Aggarwal

2

Đây là một ví dụ về cách bạn có thể sử dụng LayoutBuilderđể xác định kích thước của tiện ích con.

LayoutBuilderwidget có thể xác định các ràng buộc của widget cha, một trong những trường hợp sử dụng của nó là có thể để widget con của nó thích ứng với các kích thước của cha mẹ của chúng.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  var dimension = 40.0;

  increaseWidgetSize() {
    setState(() {
      dimension += 20;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(children: <Widget>[
          Text('Dimension: $dimension'),
          Container(
            color: Colors.teal,
            alignment: Alignment.center,
            height: dimension,
            width: dimension,
            // LayoutBuilder inherits its parent widget's dimension. In this case, the Container in teal
            child: LayoutBuilder(builder: (context, constraints) {
              debugPrint('Max height: ${constraints.maxHeight}, max width: ${constraints.maxWidth}');
              return Container(); // create function here to adapt to the parent widget's constraints
            }),
          ),
        ]),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increaseWidgetSize,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Bản giới thiệu

bản giới thiệu

Nhật ký

I/flutter (26712): Max height: 40.0, max width: 40.0
I/flutter (26712): Max height: 60.0, max width: 60.0
I/flutter (26712): Max height: 80.0, max width: 80.0
I/flutter (26712): Max height: 100.0, max width: 100.0

0

findRenderObject()trả về giá trị RenderBoxđược sử dụng để cung cấp kích thước của tiện ích con đã vẽ và nó phải được gọi sau khi cây tiện ích con được xây dựng, vì vậy nó phải được sử dụng với một số cơ chế addPostFrameCallback()gọi lại hoặc gọi lại.

class SizeWidget extends StatefulWidget {
  @override
  _SizeWidgetState createState() => _SizeWidgetState();
}

class _SizeWidgetState extends State<SizeWidget> {
  final GlobalKey _textKey = GlobalKey();
  Size textSize;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => getSizeAndPosition());
  }

  getSizeAndPosition() {
    RenderBox _cardBox = _textKey.currentContext.findRenderObject();
    textSize = _cardBox.size;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Size Position"),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Text(
            "Currern Size of Text",
            key: _textKey,
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
          ),
          SizedBox(
            height: 20,
          ),
          Text(
            "Size - $textSize",
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

Đầu ra:

nhập mô tả hình ảnh ở đây


0

sử dụng gói: z_tools . Các bước:

1. thay đổi tệp chính

void main() async {
  runZoned(
    () => runApp(
      CalculateWidgetAppContainer(
        child: Center(
          child: LocalizedApp(delegate, MyApp()),
        ),
      ),
    ),
    onError: (Object obj, StackTrace stack) {
      print('global exception: obj = $obj;\nstack = $stack');
    },
  );
}

2. sử dụng trong chức năng

_Cell(
    title: 'cal: Column-min',
    callback: () async {
        Widget widget1 = Column(
        mainAxisSize: MainAxisSize.min,
        children: [
            Container(
            width: 100,
            height: 30,
            color: Colors.blue,
            ),
            Container(
            height: 20.0,
            width: 30,
            ),
            Text('111'),
        ],
        );
        // size = Size(100.0, 66.0)
        print('size = ${await getWidgetSize(widget1)}');
    },
),

0

Cách đơn giản nhất là sử dụng MeasuredSize, đây là một widget tính toán kích thước của nó trong thời gian chạy.

Bạn có thể sử dụng nó như vậy:

MeasuredSize(
          onChange: (Size size) {
            setState(() {
              print(size);
            });
          },
          child: Text(
            '$_counter',
            style: Theme.of(context).textTheme.headline4,
          ),
        );

Bạn có thể tìm thấy nó ở đây: https://pub.dev/packages/measured_size

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.