如何在Flutter中在窗口调整大小时更新OverlayEntry的尺寸

huangapple go评论73阅读模式
英文:

How to update OverlayEntry dimensions on window resize in flutter

问题

I am trying to show an overlay of the same width as of the TextField. The below code sample works fine but when I run it in the browser and resize the window, The Textfield resizes but the overlay stays with the old dimensions.

如何在调整窗口大小时更新(而不是重新渲染)叠加层的尺寸?我知道Flutter有一个内置的AutoComplete小部件,但由于今天我们无法在叠加层周围添加填充,它存在一些问题。因此,我正在尝试构建自己的小部件。

这是我尝试的示例代码:

import 'package:flutter/material.dart';

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

// ...(省略了一些代码)...

class _MyHomePageState extends State<MyHomePage> {
  final ScrollController _scrollController = ScrollController();

  // ...(省略了一些代码)...

  final LayerLink _layerLink = LayerLink();

  OverlayEntry _createOverlay() {
    final renderBox = context.findRenderObject() as RenderBox;
    final size = renderBox.size;
    final offset = renderBox.localToGlobal(Offset.zero);
    return OverlayEntry(
        builder: (context) => Positioned(
              left: offset.dx,
              width: size.width,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));
  }

  // ...(省略了一些代码)...
}

Heres the output of the above code

Notice that Overlay dimensions do not match the window size (when window was resized)

如何在Flutter中在窗口调整大小时更新OverlayEntry的尺寸

We could force the overlay dimensions by adjusting the Positioned.right parameter

OverlayEntry(
        builder: (context) => Positioned(
              left: 0,
              right: 0, // custom offset from right
              // width: textFieldSize.width,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));

But this would fail if we were to give padding around the Textfield
e.g if I set Positioned.right:0 and Give Padding around Textfield the overlay would always take up the whole width.

如何在Flutter中在窗口调整大小时更新OverlayEntry的尺寸

The final solution I tried is to set the width of the textfield to overlay width, But I am not sure how do we update the Textfield renderBox so that the overlay takes up the expected dimensions.

OverlayEntry _createOverlay() {
    /// textfield dimensions
    final textFieldRenderBox =
        textFieldKey.currentContext!.findRenderObject() as RenderBox;
    final textFieldOffset = textFieldRenderBox.localToGlobal(Offset.zero);
    final textFieldSize = textFieldRenderBox.size;
    print('$textFieldSize $textFieldOffset ');
    return OverlayEntry(
        builder: (context) => Positioned(
              left: 0,
              width: textFieldSize.width,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));
  }

Edit: I tried using didChangeMetrics to listen to MediaQueyChanges and update the Overlay using ValueListenableBuilder but its janky and updates with a delay heres the output and the code

英文:

I am trying to show an overlay of the same width as of the TextField. The below code sample works fine but when I run it in the browser and resize the window, The Textfield resizes but the overlay stays with the old dimensions.

How can we update (not rerender the overlay) the dimensions of the overlay on resizing the window? I know flutter has a built in AutoComplete widget but it is buggy since we cannot add padding around the overlay as of today. So I am trying to build my own widget.

Here's a sample code I have tried

import &#39;package:flutter/material.dart&#39;;

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &#39;Flutter Demo&#39;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: &#39;Flutter Demo Home Page&#39;),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  final ScrollController _scrollController = ScrollController();

  Widget _list() {
    return Container(
      height: 5 * 40,
      child: Scrollbar(
          controller: _scrollController,
          thumbVisibility: true,
          child: ListView.builder(
              controller: _scrollController,
              padding: EdgeInsets.zero,
              itemCount: 20,
              itemBuilder: (context, index) =&gt; ListTile(
                    title: Text(&#39;item $index&#39;),
                    onTap: () {
                      _overlayEntry.remove();
                    },
                  ))),
    );
  }

  final LayerLink _layerLink = LayerLink();

  OverlayEntry _createOverlay() {
    final renderBox = context.findRenderObject() as RenderBox;
    final size = renderBox.size;
    final offset = renderBox.localToGlobal(Offset.zero);
    return OverlayEntry(
        builder: (context) =&gt; Positioned(
              left: offset.dx,
              width: size.width,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));
  }

  late OverlayEntry _overlayEntry;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            CompositedTransformTarget(
              link: _layerLink,
              child: TextFormField(
                decoration: InputDecoration(
                  focusedBorder: OutlineInputBorder(
                    borderSide: BorderSide(
                      color: Colors.black.withOpacity(0.8),
                    ),
                  ),
                  border: OutlineInputBorder(
                    borderSide: BorderSide(color: Colors.red),
                  ),
                ),
                onTap: () {
                   if (_overlayEntry.mounted) {
                        _overlayEntry.remove();
                   }
                   Overlay.of(context).insert(_overlayEntry);
                },
                onChanged: (query) {},
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Heres the output of the above code

Notice that Overlay dimensions do not match the window size (when window was resized)

如何在Flutter中在窗口调整大小时更新OverlayEntry的尺寸

We could force the overlay dimensions by adjusting the Positioned.right parameter

OverlayEntry(
        builder: (context) =&gt; Positioned(
              left: 0,
              right: 0, // custom offset from right
              // width: textFieldSize.width,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));

But this would fail if we were to give padding around the Textfield
e.g if I set Positioned.right:0 and Give Padding around Textfield the overlay would always take up the whole width.

如何在Flutter中在窗口调整大小时更新OverlayEntry的尺寸

The final solution I tried is to set the width of the textfield to overlay width, But I am not sure how do we update the Textfield renderBox so that the overlay takes up the expected dimensions.

  OverlayEntry _createOverlay() {
    /// textfield dimensions
    final textFieldRenderBox =
        textFieldKey.currentContext!.findRenderObject() as RenderBox;
    final textFieldOffset = textFieldRenderBox.localToGlobal(Offset.zero);
    final textFieldSize = textFieldRenderBox.size;
    print(&#39;$textFieldSize $textFieldOffset &#39;);
    return OverlayEntry(
        builder: (context) =&gt; Positioned(
              left: 0,
              width: textFieldSize.width,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));
  }

Edit: I tried using didChangeMetrics to listen to MediaQueyChanges and update the Overlay using ValueListenableBuilder but its janky and updates with a delay heres the output and the code

答案1

得分: 1

我认为你的使用情况可以通过使用AutoComplete小部件来解决,你试过了吗?

class Autocomplete<T extends Object> extends StatelessWidget {

它会显示类似于以下的内容: 图片链接

英文:

i think that your use case could be resolved by using the AutoComplete Widget, have you tried it yet ?

class Autocomplete&lt;T extends Object&gt; extends StatelessWidget {

will will show you something like this

enter image description here

答案2

得分: 0

We could solve this using OverLayPortal a new widget introduced in flutter 3.10.

原始答案在此处回答:https://github.com/flutter/flutter/issues/78746#issuecomment-1564893844

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(
          title: "My Home Page",
        ));
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.only(left:24.0,right:12.0),
              child: CustomWidget(),
            ),
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline6,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

class CustomWidget extends StatefulWidget {
  const CustomWidget({Key? key}) : super(key: key);
  @override
  State<CustomWidget> createState() => _CustomWidgetState();
}

class _CustomWidgetState extends State<CustomWidget> {
  final ScrollController _scrollController = ScrollController();

  Widget _list() {
    return Container(
      height: 5 * 40,
      child: Scrollbar(
          controller: _scrollController,
          thumbVisibility: true,
          child: ListView.builder(
              controller: _scrollController,
              padding: EdgeInsets.zero,
              itemCount: 20,
              itemBuilder: (context, index) => ListTile(
                    title: Text('item $index'),
                    onTap: () {
                      _portalController.hide();
                    },
                  ))),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  GlobalKey gkey = GlobalKey();
  final OverlayPortalController _portalController = OverlayPortalController();
  @override
  Widget build(BuildContext context) {
    Widget textfield() => TextFormField(
          key: gkey,
          decoration: InputDecoration(
            focusedBorder: OutlineInputBorder(
              borderSide: BorderSide(
                color: Colors.black.withOpacity(0.8),
              ),
            ),
            border: const OutlineInputBorder(
              borderSide: BorderSide(color: Colors.red),
            ),
          ),
          onTap: () {
            if (!_portalController.isShowing) {
              _portalController.show();
            }
          },
          onChanged: (query) {},
        );
    
    return OverlayPortal(
      controller: _portalController,
      overlayChildBuilder: (BuildContext context) {
        final renderBox = gkey.currentContext!.findRenderObject() as RenderBox;
        final Size tSize = renderBox.size;
        final Offset offset = renderBox.localToGlobal(Offset.zero);
        return Positioned(
          left: offset.dx,
          top: offset.dy + tSize.height, // + textfield height
          child: Material(
            color: Colors.red,
            child: SizedBox(
              width: tSize.width,
              child: _list(),
            ),
          ),
        );
      },
      child: textfield(),
    );
  }
}
英文:

We could solve this using OverLayPortal a new widget introduced in flutter 3.10.

Originally answered here https://github.com/flutter/flutter/issues/78746#issuecomment-1564893844

import &#39;package:flutter/material.dart&#39;;

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: &#39;Flutter Demo&#39;,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(
          title: &quot;My Home Page&quot;,
        ));
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Padding(
              padding: EdgeInsets.only(left:24.0,right:12.0),
              child: CustomWidget(),
            ),
            const Text(
              &#39;You have pushed the button this many times:&#39;,
            ),
            Text(
              &#39;$_counter&#39;,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: &#39;Increment&#39;,
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class CustomWidget extends StatefulWidget {
  const CustomWidget({Key? key}) : super(key: key);
  @override
  State&lt;CustomWidget&gt; createState() =&gt; _CustomWidgetState();
}

class _CustomWidgetState extends State&lt;CustomWidget&gt; {
  final ScrollController _scrollController = ScrollController();

  Widget _list() {
    return Container(
      height: 5 * 40,
      child: Scrollbar(
          controller: _scrollController,
          thumbVisibility: true,
          child: ListView.builder(
              controller: _scrollController,
              padding: EdgeInsets.zero,
              itemCount: 20,
              itemBuilder: (context, index) =&gt; ListTile(
                    title: Text(&#39;item $index&#39;),
                    onTap: () {
                      _portalController.hide();
                    },
                  ))),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  GlobalKey gkey = GlobalKey();
  final OverlayPortalController _portalController = OverlayPortalController();
  @override
  Widget build(BuildContext context) {
    Widget textfield() =&gt; TextFormField(
          key: gkey,
          decoration: InputDecoration(
            focusedBorder: OutlineInputBorder(
              borderSide: BorderSide(
                color: Colors.black.withOpacity(0.8),
              ),
            ),
            border: const OutlineInputBorder(
              borderSide: BorderSide(color: Colors.red),
            ),
          ),
          onTap: () {
            if (!_portalController.isShowing) {
              _portalController.show();
            }
          },
          onChanged: (query) {},
        );
    
    return OverlayPortal(
      controller: _portalController,
      overlayChildBuilder: (BuildContext context) {
        final renderBox = gkey.currentContext!.findRenderObject() as RenderBox;
        final Size tSize = renderBox.size;
        final Offset offset = renderBox.localToGlobal(Offset.zero);
        return Positioned(
          left: offset.dx,
          top: offset.dy + tSize.height, // + textfield height
          child: Material(
            color: Colors.red,
            child: SizedBox(
              width: tSize.width,
              child: _list(),
            ),
          ),
        );
      },
      child: textfield(),
    );
  }
}

huangapple
  • 本文由 发表于 2023年5月20日 21:01:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76295364.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定