Skip to content

Instantly share code, notes, and snippets.

@maheshmnj
Last active May 26, 2023 19:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maheshmnj/7b3f135f090647c6ab64fb49e207f495 to your computer and use it in GitHub Desktop.
Save maheshmnj/7b3f135f090647c6ab64fb49e207f495 to your computer and use it in GitHub Desktop.
Overlay dimensions cannot be updated on window resize.
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: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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: () {
_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) => 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: <Widget>[
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) {},
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
@maheshmnj
Copy link
Author

maheshmnj commented May 20, 2023

Overlay dimensions do not update when the window size changes

Screenshot 2023-05-19 at 9 29 35 PM

@maheshmnj
Copy link
Author

We could make the overlay take the whole width by setting Positioned.right:0

....
 return OverlayEntry(
        builder: (context) => Positioned(
              left: offset.dx,
              right: 0,
              child: CompositedTransformFollower(
                  offset: Offset(0, 50),
                  link: _layerLink,
                  child: Material(color: Colors.red, child: _list())),
            ));
...

But it would fail for a case when the text field has padding around

 Padding(
              padding: const EdgeInsets.all(18.0),
              child: CompositedTransformTarget(
...
...

image

@maheshmnj
Copy link
Author

I tried using didChangeMetrics to listen to MediaQueyChanges and update the Overlay using ValueListenableBuilder but its janky and updates with a delay

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: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  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: () {
                      _overlayEntry.remove();
                    },
                  ))),
    );
  }

  final LayerLink _layerLink = LayerLink();

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

  late OverlayEntry _overlayEntry;

  @override
  void didChangeMetrics() {
    print('didChangeMetrics');
    size = MediaQuery.of(context).size;
    sizeNotifier.value = size;
    setState(() {});
  }

  @override
  void dispose() {
    sizeNotifier.dispose();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _overlayEntry = _createOverlay();
      size = MediaQuery.of(context).size;
      sizeNotifier.value = size;
    });
    super.initState();
  }

  ValueNotifier<Size> sizeNotifier = ValueNotifier(Size.zero);

  final textFieldKey = GlobalKey();
  Size size = Size.zero;
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(18.0),
                child: CompositedTransformTarget(
                  link: _layerLink,
                  child: TextFormField(
                    key: textFieldKey,
                    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) {},
                  ),
                ),
              ),
            ],
          ),
        ),
      );
    });
  }
}
Screen.Recording.2023-05-20.at.09.24.20.mov

@maheshmnj
Copy link
Author

maheshmnj commented May 25, 2023

Using Overlay portal instead of overlay entry

code sample
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.all(8.0),
              child: CustomWidget(),
            ),
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        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<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();
    WidgetsBinding.instance.addPostFrameCallback((x) {
      final renderBox = gkey.currentContext!.findRenderObject() as RenderBox;
      tSize = renderBox.size;
      offset = renderBox.localToGlobal(Offset.zero);
    });
  }

  late Offset offset;
  late Size tSize;
  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: () {
            print("offset: $offset, size= $tSize");
            if (!_portalController.isShowing) {
              _portalController.show();
            }
          },
          onChanged: (query) {},
        );
    
    final size = MediaQuery.of(context).size;
//     final rightOffset = size.width - tSize.width - offset.dx;
    return OverlayPortal(
        controller: _portalController,
        overlayChildBuilder: (BuildContext context) {
          return Positioned(
              right: 0,
              left: offset.dx,
              top: offset.dy + tSize.height, // + textfield height
              child: Material(color: Colors.red, child: _list()));
        },
        child: textfield());
  }
}

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment