Reputation: 185
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:
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.
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.
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
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
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