Tap IconButton in AppBar to display TextField floating over Scaffold body (with one widget, not two)

Viewed 198

Is there a way to float a box from a Widget in the AppBar over (above) the Scaffold body? For example, if I put my search Widget into the Scaffold body, when my search IconButton is tapped, this TextField appears and hangs over the content below it:

Search icon with search input box

However, ideally, I'd like the search icon to appear in the AppBar (not the Scaffold body), and then when tapped, I'd like the input box to float over the Scaffold body. If I put my Search Widget into the AppBar, the TextField is hidden under the Scaffold body, like this:

Search icon in AppBar overflowing behind Scaffold

If you look carefully, you can actually see the TextField focusedBorder after I tap in the small margin below the icon on the AppBar but above the Scaffold body:

TextField border when tapped between AppBar and Scaffold

So, you can see that the TextField is hidden behind the Scaffold body.

I discovered from the Flutter documentation that the Scaffold body puts its content below the AppBar and behind the floatingActionButton and drawer by design (https://api.flutter.dev/flutter/material/Scaffold/body.html). However, using a floatingActionButton or a drawer for this use case would be a bit of a hack.

I suppose that I could have the TextField in a separate widget and located in the Scaffold body and use a Provider/ChangeNotifier to communicate to the widget in the Scaffold that the IconButton in the AppBar was tapped. But, that splits the component logic into two places, which seems messy. I'm hoping someone can enlighten me with a best practice or set me in the right direction.

So, is there an appropriate way to have an IconButton in an AppBar display a TextField that floats over the Scaffold body? (It seems like it would be a common use case.)

1 Answers

You could use a Stack for your Scaffold > body:

enter image description here

Full source code:

import 'dart:math' show Random;

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

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

class AppWidget extends StatelessWidget {
  const AppWidget({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => SearchModel(),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(primaryColor: Color(0xfffedbd0)),
        title: 'Flutter Demo',
        home: HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<SearchModel>(
      builder: (context, searchModel, child) => Scaffold(
        appBar: AppBar(
          title: Text('SHRINE'),
          actions: [
            IconButton(
                icon: Icon(Icons.search, semanticLabel: 'search'),
                onPressed: () => searchModel.toggleVisibility()),
          ],
        ),
        body: Container(
          padding: EdgeInsets.all(16.0),
          alignment: Alignment.center,
          child: Search(),
        ),
      ),
    );
  }
}

class Search extends StatelessWidget {
  Search();

  @override
  Widget build(BuildContext context) {
    return Consumer<SearchModel>(builder: (context, searchModel, child) {
      return Stack(
        children: [
          GridView.count(
            crossAxisCount: 3,
            children: List.generate(
              30,
              (index) => Container(
                color: Color(0xaaaa0000 + 0xffffff ~/ (index + 1)),
              ),
            ),
          ),
          Positioned.fill(
            child: Align(
              alignment: Alignment.center,
              child: AnimatedOpacity(
                opacity: searchModel.isVisible ? 1.0 : 0.0,
                duration: Duration(milliseconds: 500),
                child: SizedBox(
                  width: 190.0,
                  height: 25.0,
                  child: TextField(
                    decoration: InputDecoration(
                      filled: true,
                      fillColor: Theme.of(context).primaryColor,
                      hintText: 'Enter a search term',
                      contentPadding: const EdgeInsets.symmetric(
                          horizontal: 8.0, vertical: 4.0),
                      enabledBorder: OutlineInputBorder(
                        borderSide: BorderSide(color: Colors.black),
                        borderRadius: BorderRadius.circular(25.7),
                      ),
                      focusedBorder: OutlineInputBorder(
                        borderSide: BorderSide(color: Colors.black),
                        borderRadius: BorderRadius.circular(25.7),
                      ),
                    ),
                    onChanged: (text) => searchModel.updateText(text),
                  ),
                ),
              ),
            ),
          ),
        ],
      );
    });
  }
}

class SearchModel extends ChangeNotifier {
  String text = '';
  bool isVisible = false;

  updateText(String newText) {
    text = newText;
    notifyListeners();
  }

  toggleVisibility() {
    isVisible = !isVisible;
    notifyListeners();
  }
}
Related