简体   繁体   中英

Push OverlayEntry Widget above keyboard

I have created a custom widget where an overlay entry widget is shown right bottom of it with a search bar and a list.

The problem I am facing is, when the user clicks the textfield inside the overlay widget, the screen does not push up enough for the user to see the textfield. Please check the screenshoot for more details

The code of the custom dropdown button widget is below:

import 'package:flutter/material.dart';

class DropDownWidget2 extends StatefulWidget {
  DropDownWidget2({Key? key}) : super(key: key);

  @override
  State<DropDownWidget2> createState() => _DropDownWidget2State();
}

class _DropDownWidget2State extends State<DropDownWidget2> with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;
  bool isOpen = false;

  final List<Map<String, dynamic>> _allData = [
    {"id": 1, "name": "Andy", "age": 29},
    {"id": 2, "name": "Aragon", "age": 40},
    {"id": 3, "name": "Bob", "age": 5},
    {"id": 4, "name": "Barbara", "age": 35},
    {"id": 5, "name": "Candy", "age": 21},
    {"id": 6, "name": "Colin", "age": 55},
    {"id": 7, "name": "Audra", "age": 30},
    {"id": 8, "name": "Banana", "age": 14},
    {"id": 9, "name": "Caversky", "age": 100},
    {"id": 10, "name": "Becky", "age": 32},
  ];

  // This list holds the data for the list view
  List<Map<String, dynamic>> _foundData = [];

  final layerLink = LayerLink();
  OverlayEntry? entry;

  @override
  void initState() {
    _animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 450));
    _animation = Tween(begin: 0.0, end: -.5)
        .animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOut));
    _foundData = _allData;

    super.initState();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _handleOnPressed() {
    setState(() {
      isOpen = !isOpen;
      isOpen ? _animationController.forward() : _animationController.reverse();
      showOverLay();
    });
  }

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: layerLink,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 10),
        decoration: BoxDecoration(
            border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(10)),
        child: Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('Select one from below'),
            IconButton(
              padding: EdgeInsets.zero,
              iconSize: 18,
              splashRadius: 20,
              splashColor: Colors.greenAccent,
              icon: RotationTransition(
                turns: _animation,
                child: Icon(Icons.expand_more),
              ),
              onPressed: () => _handleOnPressed(),
            )
          ],
        ),
      ),
    );
  }

  Widget itemsWidget() {
    return Container(
      width: MediaQuery.of(context).size.width,
      decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
      padding: EdgeInsets.only(left: 12, top: 4, bottom: 4),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          TextField(
            onChanged: (value) => {},
            decoration: const InputDecoration(labelText: 'Search', suffixIcon: Icon(Icons.search)),
          ),
          _foundData.isNotEmpty
              ? ListView.separated(
                  padding: EdgeInsets.zero,
                  shrinkWrap: true,
                  itemCount: _foundData.length,
                  itemBuilder: (context, index) {
                    return Text(_foundData[index]['name']);
                  },
                  separatorBuilder: (context, index) {
                    return Divider();
                  },
                )
              : const Text(
                  'No results found',
                  style: TextStyle(fontSize: 14),
                ),
        ],
      ),
    );
  }

  Widget buildOverlay() {
    return Material(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
        side: const BorderSide(color: Colors.black, width: 1),
      ),
      child: AnimatedCrossFade(
        duration: const Duration(microseconds: 500),
        firstChild: Container(), // When you don't want to show menu..
        secondChild: itemsWidget(),
        crossFadeState: isOpen ? CrossFadeState.showSecond : CrossFadeState.showFirst,
      ),
    );
  }

  void showOverLay() {
    final overlay = Overlay.of(context)!;
    final renderBox = context.findRenderObject() as RenderBox;
    final size = renderBox.size;

    entry = OverlayEntry(
        builder: (context) => Positioned(
            width: size.width,
            child: CompositedTransformFollower(
                link: layerLink,
                showWhenUnlinked: false,
                offset: Offset(0, size.height),
                child: buildOverlay())));

    overlay.insert(entry!);
  }
}

As you can observe from the screenshot. The keyboard moves above overlayentry hiding my textfield and my list items. Is there any way I can push the whole overlayEntry above the keyboard? I've tried many ways like using mediaquery bottom insets and other things but i still could not achieve it.

无键盘 . 带键盘

Maybe you could try one of below:

  • add a padding to the page widget;
  • add a padding to the text field;

On the 1st you may add padding wrapping the body:

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: MediaQuery.of(context).viewInsets,
      child: The page ...,
    );
  }

With the 2nd option you may add bottom padding equals to the keyboard size. At the page (StatefulWidget), you may add the didChangeDependencies :

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    setState(() {
      _keyboardSize = MediaQuery.of(context).viewInsets.bottom;
    });
  }

And in your TextField, you set the scrollPadding property:

  scrollPadding: EdgeInsets.only(
    bottom: _keyboardSize,
  ),

you can try this if that's what you really need to do:

Widget itemsWidget() {
                      return Container(
                        width: MediaQuery.of(context).size.width,
                        decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
                        padding: EdgeInsets.only(left: 12, top: 4, bottom: 4),
                        child: SingleChildScrollView(
                          child: Column(
                            mainAxisSize: MainAxisSize.min,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              TextField(
                                onChanged: (value) => {},
                                onTap: (){
                                  // here you can control the phone's default keyboard
                                  FocusScope.of(context).unfocus();
                                },
                                decoration: const InputDecoration(labelText: 'Search', suffixIcon: Icon(Icons.search)),
                              ),
                              _foundData.isNotEmpty
                                  ? ListView.separated(
                                padding: EdgeInsets.zero,
                                shrinkWrap: true,
                                itemCount: _foundData.length,
                                itemBuilder: (context, index) {
                                  return Text(_foundData[index]['name']);
                                },
                                separatorBuilder: (context, index) {
                                  return Divider();
                                },
                              )
                                  : const Text(
                                'No results found',
                                style: TextStyle(fontSize: 14),
                              ),
                            ],
                          ),
                        ),
                      );
                    }

I think you should allow it to be scrollable

Try this

you can use some of this 

return Scaffold(
      body: SafeArea(
        child: Center(
          child: Stack(

then on your ItemsWidget

return Container(
      padding: const EdgeInsets.all(16.0),
      child: SingleChildScrollView(
        reverse: true,
        child: Form(

Some thing along these lines, i edited your code

import 'package:flutter/material.dart';

class DropDownWidget2 extends StatefulWidget {
  const DropDownWidget2({Key? key}) : super(key: key);

  @override
  State<DropDownWidget2> createState() => _DropDownWidget2State();
}

class _DropDownWidget2State extends State<DropDownWidget2>
    with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;
  bool isOpen = false;

  final List<Map<String, dynamic>> _allData = [
    {"id": 1, "name": "Andy", "age": 29},
    {"id": 2, "name": "Aragon", "age": 40},
    {"id": 3, "name": "Bob", "age": 5},
    {"id": 4, "name": "Barbara", "age": 35},
    {"id": 5, "name": "Candy", "age": 21},
    {"id": 6, "name": "Colin", "age": 55},
    {"id": 7, "name": "Audra", "age": 30},
    {"id": 8, "name": "Banana", "age": 14},
    {"id": 9, "name": "Caversky", "age": 100},
    {"id": 10, "name": "Becky", "age": 32},
  ];

  // This list holds the data for the list view
  List<Map<String, dynamic>> _foundData = [];

  final layerLink = LayerLink();
  OverlayEntry? entry;

  @override
  void initState() {
    _animationController = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 450));
    _animation = Tween(begin: 0.0, end: -.5).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.easeOut));
    _foundData = _allData;

    super.initState();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _handleOnPressed() {
    setState(() {
      isOpen = !isOpen;
      isOpen ? _animationController.forward() : _animationController.reverse();
      showOverLay();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Stack(
            children: [
              Container(
                alignment: Alignment.center,
                child: CompositedTransformTarget(
                  link: layerLink,
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 4),
                    decoration: BoxDecoration(
                        border: Border.all(color: Colors.black),
                        borderRadius: BorderRadius.circular(10)),
                    child: Row(
                      mainAxisSize: MainAxisSize.max,
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text('Select one from below'),
                        IconButton(
                          padding: EdgeInsets.zero,
                          iconSize: 18,
                          splashRadius: 10,
                          splashColor: Colors.greenAccent,
                          icon: RotationTransition(
                            turns: _animation,
                            child: const Icon(Icons.expand_more),
                          ),
                          onPressed: () => _handleOnPressed(),
                        )
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget itemsWidget() {
    return Container(
        width: MediaQuery.of(context).size.width,
        decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
        padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
        child: SingleChildScrollView(
          reverse: true,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              TextField(
                onChanged: (value) => {},
                decoration: const InputDecoration(
                    labelText: 'Search', suffixIcon: Icon(Icons.search)),
              ),
              _foundData.isNotEmpty
                  ? ListView.separated(
                      padding: EdgeInsets.zero,
                      shrinkWrap: true,
                      itemCount: _foundData.length,
                      itemBuilder: (context, index) {
                        return Text(_foundData[index]['name']);
                      },
                      separatorBuilder: (context, index) {
                        return const Divider();
                      },
                    )
                  : const Text(
                      'No results found',
                      style: TextStyle(fontSize: 14),
                    ),
            ],
          ),
        ));
  }

  Widget buildOverlay() {
    return Material(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
        side: const BorderSide(color: Colors.black, width: 1),
      ),
      child: AnimatedCrossFade(
        duration: const Duration(microseconds: 500),
        firstChild: Container(), // When you don't want to show menu..
        secondChild: itemsWidget(),
        crossFadeState:
            isOpen ? CrossFadeState.showSecond : CrossFadeState.showFirst,
      ),
    );
  }

  void showOverLay() {
    final overlay = Overlay.of(context)!;
    final renderBox = context.findRenderObject() as RenderBox;
    final size = renderBox.size;

    entry = OverlayEntry(
        builder: (context) => Positioned(
            width: size.width,
            child: CompositedTransformFollower(
                link: layerLink,
                showWhenUnlinked: false,
                offset: Offset(0, size.height),
                child: buildOverlay())));

    overlay.insert(entry!);
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM