Jeffrey
Jeffrey

Reputation: 185

Flutter Layout with Fixed Bottom Section and Scrollable Upper SEction

I am currently writing a Flutter app for a kiosk-like device. This device will be mounted in landscape mode and has an integrated barcode scanner on the bottom.

The device will spend almost all of its time on a single layout:

Standard Layout w/o Keyboard

Currently, the entire body is in a SingleChildScrollView. This allows the view to 'slide up' when the user taps in text input box. Then when keyboard closes, the view 'slides' back down.

With Keyboard Active

What I'm trying to do is have the bottom "Scan Ticket Below" row to be fixed to the bottom of the view, at least when visible (when keyboard not covering it). As of now, it's a flex layout and it' doesn't quite get to the bottom.

Look at image with debug paint: the pink box should be at bottom, and everything above it should be in a scrollable view. View with Debug Box Paint

I started messing around with a number of options. I don't want fixed positions as we will ultimately also make the solution available on iDevices with various screen sizes.

Here is my current Scaffold Body:

Container(
      constraints: BoxConstraints.expand(),
      child: SingleChildScrollView(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: [
              Padding(
                padding: EdgeInsets.only(top: 50.0, bottom: 20.0),
                child: Text(_message ?? "Welcome!",
                textAlign: TextAlign.center,
                    style:
                        TextStyle(fontSize: 45.0, color: Colors.black)),
              ),
                                  Padding(
                padding: EdgeInsets.all(20.0),
                child: Text("Scan ticket below or Search for your Transaction",
                    style:
                        TextStyle(fontSize: 25.0, color: Colors.black)),
              ),
              Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  mainAxisSize: MainAxisSize.max,
                  children: [
                    Expanded(
                        flex: 4,
                        child: Container(
                            margin: EdgeInsets.symmetric(
                                horizontal: 50.0, vertical: 25.0),
                            color: skidataThemeData.primaryColor,
                            padding: const EdgeInsets.symmetric(
                                horizontal: 25.0, vertical: 25.0),
                            child: Center(
                              child: new TextFormField(
                                //This autofocus works, but during transition from successful plate val to
                                //this page, the keyoard is activated during transition causing an overflow on the
                                //applyValidationScreen.
                                //autofocus: true,
                                style: new TextStyle(
                                    decorationColor:
                                        skidataThemeData.accentColor,
                                    fontSize: 90.0,
                                    color: skidataThemeData.accentColor),
                                textAlign: TextAlign.center,
                                onSaved: (String value) {
                                  this._data.plateNumber = value;
                                },
                                decoration: new InputDecoration(
                                    hintText: "Enter Data",
                                    hintStyle: new TextStyle(
                                        color: Colors.white),
                                    fillColor:
                                        skidataThemeData.accentColor,
                                    contentPadding: EdgeInsets.all(1.0),
                                    border: InputBorder.none),
                                validator: (value) {
                                  if (value.isEmpty) {
                                    return 'Field cannot be blank.';
                                  }
                                },
                                autocorrect: false,
                              ),
                            ))),
                    Expanded(
                        flex: 1,
                        child: Padding(
                          padding: const EdgeInsets.all(25.0),
                          child: RaisedButton(
                            padding: EdgeInsets.all(15.0),
                            color: skidataThemeData.accentColor,
                            onPressed: () async {
                              FocusScope.of(context)
                                  .requestFocus(new FocusNode());
                              setState(() {
                                _message = '';
                              });

                              if (_formKey.currentState.validate()) {
                                // If the form is valid, we want to show a Snackbar
                                Scaffold.of(context).showSnackBar(
                                  new SnackBar(
                                      content: Row(
                                        mainAxisAlignment:
                                            MainAxisAlignment
                                                .spaceBetween,
                                        children: [
                                          new CircularProgressIndicator(
                                            valueColor:
                                                new AlwaysStoppedAnimation<
                                                        Color>(
                                                    skidataThemeData
                                                        .primaryColor),
                                          ),
                                          Text(
                                              'Search for matching record..',
                                              style: new TextStyle(
                                                  color: skidataThemeData
                                                      .primaryColor))
                                        ],
                                      ),
                                      backgroundColor:
                                          skidataThemeData.accentColor,
                                      duration: Duration(seconds: 10)),
                                );
                                await new Future.delayed(
                                    const Duration(milliseconds: 1000));

                                Scaffold.of(context)
                                    .hideCurrentSnackBar();

                                Navigator.push(
                                  context,
                                  new MaterialPageRoute(
                                      builder: (context) =>
                                          new SvalKioskApp()),
                                );
                              } else {
                                setState(() {
                                  _message = "";
                                });
                              }
                            },
                            child: new Text('Search for Record',
                                textAlign: TextAlign.center,
                                style: TextStyle(
                                    fontSize: 25.0, color: Colors.black)),
                          ),
                        ))
                  ]),
              Container(
                color: Colors.pink,
                child: Padding(
                  padding: const EdgeInsets.only(top:40.0),
                  child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
                    Expanded(
                        flex: 1,
                        child: Container(
                          alignment: Alignment.centerRight,
                          padding: EdgeInsets.symmetric(
                              horizontal: 10.0, vertical: 5.0),
                          child: Image.asset(
                            'images/downarrow.png',
                            fit: BoxFit.contain,
                          ),
                        )),
                    Expanded(
                        flex: 5,
                        child: Center(
                            child: Text("Scan Physical Ticket Below!",
                                style: TextStyle(
                                    fontWeight: FontWeight.bold,
                                    fontSize: 45.0)))),
                    Expanded(
                        flex: 1,
                        child: Container(
                          alignment: Alignment.centerLeft,
                          padding: EdgeInsets.symmetric(
                              horizontal: 10.0, vertical: 5.0),
                          child: Image.asset(
                            'images/downarrow.png',
                            fit: BoxFit.contain,
                          ),
                        )),
                  ]),
                ),
              )
            ]), 
      ),
    )

Upvotes: 1

Views: 7433

Answers (2)

leodriesch
leodriesch

Reputation: 5780

How about replacing the SingleChildScrollView with a Column and then setting the resizeToAvoidBottomPadding property of the Scaffold to false so you don't need the scrollview, since the keyboard will not force the layout to resize.

Upvotes: 0

Jeffrey
Jeffrey

Reputation: 185

The solution hit me when I was in the shower this morning:

Start with a column that you break into two Expanded widgets to get the top/bottom section ration desired.

Then in each Expanded, make a child container expanded to fill space. In the top container, set alignment to topCenter and set bottom container alignment to bottomCenter

In the top container, add a SingleChildScrollView child. Now the upper section is scrollable and bottom is fixed.

  Widget getInputView() {
return Builder(
    builder: (context) => Container(
          constraints: BoxConstraints.expand(),
          child: Column(
            children: <Widget>[
              Expanded(
                  flex: 4,
                  child: Container(
                      alignment: Alignment.topCenter,
                      constraints: BoxConstraints.expand(),
                      child: SingleChildScrollView(
                        child: Form(
                          key: _formKey,
                          child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                Padding(
                                  padding: const EdgeInsets.all(10.0),
                                  child: Row(
                                      mainAxisAlignment:
                                          MainAxisAlignment.end,
                                      mainAxisSize: MainAxisSize.max,
                                      children: [
                                        Text("${_kiosk.kioskName}",
                                            textAlign: TextAlign.end)
                                      ]),
                                ),
                                Padding(
                                  padding: EdgeInsets.symmetric(vertical:30.0),
                                  child: Text(
                                      _kiosk.displayMessage ?? "Welcome!",
                                      textAlign: TextAlign.center,
                                      style: TextStyle(
                                          fontSize: 45.0,
                                          color: Colors.black)),
                                ),
                                Padding(
                                  padding: EdgeInsets.all(20.0),
                                  child: Text(
                                      "Scan barcode below or enter search data in box.",
                                      style: TextStyle(
                                          fontSize: 25.0,
                                          color: Colors.black)),
                                ),
                                Row(
                                    mainAxisAlignment:
                                        MainAxisAlignment.center,
                                    mainAxisSize: MainAxisSize.max,
                                    children: [
                                      Expanded(
                                          flex: 4,
                                          child: Container(
                                              margin: EdgeInsets.symmetric(
                                                  horizontal: 50.0,
                                                  vertical: 25.0),
                                              color: themeData
                                                  .primaryColor,
                                              padding: const EdgeInsets
                                                      .symmetric(
                                                  horizontal: 25.0,
                                                  vertical: 25.0),
                                              child: Center(
                                                child: new TextFormField(
                                                  //This autofocus works, but during transition from successful plate val to
                                                  //this page, the keyoard is activated during transition causing an overflow on the
                                                  //applyValidationScreen.
                                                  //autofocus: true,
                                                  style: new TextStyle(
                                                      decorationColor:
                                                          themeData
                                                              .accentColor,
                                                      fontSize: 90.0,
                                                      color:
                                                          themeData
                                                              .accentColor),
                                                  textAlign:
                                                      TextAlign.center,
                                                  onSaved: (String value) {
                                                    this._data.plateNumber =
                                                        value;
                                                  },
                                                  decoration: new InputDecoration(
                                                      hintText:
                                                          "Enter Data",
                                                      hintStyle:
                                                          new TextStyle(
                                                              color: Colors
                                                                  .white),
                                                      fillColor:
                                                          themeData
                                                              .accentColor,
                                                      contentPadding:
                                                          EdgeInsets.all(
                                                              1.0),
                                                      border:
                                                          InputBorder.none),
                                                  validator: (value) {
                                                    if (value.isEmpty) {
                                                      return 'Field cannot be blank.';
                                                    }
                                                  },
                                                  autocorrect: false,
                                                ),
                                              ))),
                                      Expanded(
                                          flex: 1,
                                          child: Padding(
                                            padding:
                                                const EdgeInsets.all(25.0),
                                            child: RaisedButton(
                                              padding: EdgeInsets.all(15.0),
                                              color: themeData
                                                  .accentColor,
                                              onPressed: () async {
                                                FocusScope.of(context)
                                                    .requestFocus(
                                                        new FocusNode());

                                                if (_formKey.currentState
                                                    .validate()) {
                                                  // If the form is valid, we want to show a Snackbar
                                                  Scaffold.of(context)
                                                      .showSnackBar(
                                                    new SnackBar(
                                                        content: Row(
                                                          mainAxisAlignment:
                                                              MainAxisAlignment
                                                                  .spaceBetween,
                                                          children: [
                                                            new CircularProgressIndicator(
                                                              valueColor: new AlwaysStoppedAnimation<
                                                                      Color>(
                                                                  themeData
                                                                      .primaryColor),
                                                            ),
                                                            Text(
                                                                'Search for matching ticket..',
                                                                style: new TextStyle(
                                                                    color: themeData
                                                                        .primaryColor))
                                                          ],
                                                        ),
                                                        backgroundColor:
                                                            themeData
                                                                .accentColor,
                                                        duration: Duration(
                                                            seconds: 10)),
                                                  );
                                                  await new Future.delayed(
                                                      const Duration(
                                                          milliseconds:
                                                              1000));
                                                  PlateMatchResponse resp =
                                                      await this.submit();

                                                  Scaffold.of(context)
                                                      .hideCurrentSnackBar();

                                                  Navigator.push(
                                                    context,
                                                    new MaterialPageRoute(
                                                        builder: (context) =>
                                                            new SvalKioskApp()),
                                                  );
                                                }
                                              },
                                              child: new Text(
                                                  'Search for Data Match',
                                                  textAlign:
                                                      TextAlign.center,
                                                  style: TextStyle(
                                                      fontSize: 25.0,
                                                      color: Colors.black)),
                                            ),
                                          ))
                                    ]),
                              ]),
                        ),
                      ))),
              Expanded(
                  flex: 1,
                  child: Container(
                      padding: EdgeInsets.all(25.0),
                      alignment: Alignment.bottomCenter,
                      constraints: BoxConstraints.expand(),
                      child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Expanded(
                                flex: 1,
                                child: Container(
                                  alignment: Alignment.centerRight,
                                  padding: EdgeInsets.symmetric(
                                      horizontal: 10.0, vertical: 5.0),
                                  child: Image.asset(
                                    'images/downarrow.png',
                                    fit: BoxFit.contain,
                                  ),
                                )),
                            Expanded(
                                flex: 5,
                                child: Center(
                                    child: Text(
                                        "Scan Physical Ticket Below",
                                        style: TextStyle(
                                            fontWeight: FontWeight.bold,
                                            fontSize: 45.0)))),
                            Expanded(
                                flex: 1,
                                child: Container(
                                  alignment: Alignment.centerLeft,
                                  padding: EdgeInsets.symmetric(
                                      horizontal: 10.0, vertical: 5.0),
                                  child: Image.asset(
                                    'images/downarrow.png',
                                    fit: BoxFit.contain,
                                  ),
                                )),
                          ])))
            ],
          ),
        ));

Upvotes: 7

Related Questions