Is there a way to have a widget body take up a little less than the available space when the keyboard shows up (to account for a BottomSheet fixed right above the keyboard?
I have a bottom sheet that I am displaying whenever the keyboard is available, taking up 65px of space.
Underneath that is a TextFormField, with a button underneath it. The TextFormField adds additional lines as I type them, and when I don't have the share sheet up, the last line stays just visible on the screen.
I want this expected space to be 65px shorter, so the TextField starts scrolling 65px sooner (to account for the bottom sheet appended above the keyboard).
Is there a way to make this work? My current code seems like it should work, but the widget still has the same scrolling behavior, as if the bottom sheet doesn't exist.
Everything else I've tried (using a maxHeight for my BoxConstraints widget, adding an Expanded widget outside of the column) just causes a bunch of errors and the widgets don't load properly.
Current widget (the Scaffold Stateful Widget is where all this is configured):
class TweetDraftPage extends StatelessWidget {
Thread? thread;
TweetDraftPage({this.thread, super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ThreadWatcherBloc>(
create: (context) => getIt<ThreadWatcherBloc>(),
),
BlocProvider<ThreadEditorBloc>(
create: (context) => ThreadEditorBloc(
getIt<ThreadRepository>(),
thread,
getIt<ThreadWatcherBloc>(),
),
),
],
child: TweetDraftPageScaffold(),
);
}
}
class TweetDraftPageScaffold extends StatefulWidget {
const TweetDraftPageScaffold({
super.key,
});
@override
State<TweetDraftPageScaffold> createState() => _TweetDraftPageScaffoldState();
}
class _TweetDraftPageScaffoldState extends State<TweetDraftPageScaffold>
with WidgetsBindingObserver {
bool isKeyboardVisible = false;
double keyboardHeight = 0;
final double bottomSheetHeight = 65;
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
final mediaQueryData = MediaQuery.of(context);
final keyboardVisibleHeight = mediaQueryData.viewInsets.bottom;
setState(() {
isKeyboardVisible = keyboardVisibleHeight > 0;
keyboardHeight = keyboardVisibleHeight;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).canvasColor,
appBar: AppBar(
title: const Text(
'Edit Tweet',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(builder: (context, constraints) {
final double availableHeight = math.max(
constraints.maxHeight -
keyboardHeight -
(isKeyboardVisible ? bottomSheetHeight : 0),
0,
);
return SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: availableHeight),
child: Column(
children: [
// for each TweetEditorBloc in ThreadEditorBloc, create a BlocProvider
// with the TweetEditorBloc, and a child of TweetDraft
BlocConsumer<ThreadEditorBloc, ThreadEditorState>(
listenWhen: (p, c) =>
c.threadSaved != null && c.threadSaved != p.threadSaved,
listener: (context, state) {
Navigator.pop(context);
},
buildWhen: (p, c) =>
p.tweetEditorBlocs.length != c.tweetEditorBlocs.length,
builder: (context, state) {
return Form(
child: Column(
children: [
for (var i = 0;
i < state.tweetEditorBlocs.length;
i++)
BlocProvider<TweetEditorBloc>(
create: (context) => state.tweetEditorBlocs[i],
child: TweetDraft(),
),
],
),
);
},
),
SizedBox(height: 8),
ElevatedButton(
onPressed: () {
context
.read<ThreadEditorBloc>()
.add(const ThreadEditorEvent.saveThread());
context
.read<ThreadWatcherBloc>()
.add(const ThreadWatcherEvent.refresh());
},
child: Text('Save Draft')),
],
),
),
);
}),
),
bottomSheet: isKeyboardVisible
? BottomSheet(
constraints: BoxConstraints(maxHeight: bottomSheetHeight),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero),
enableDrag: false,
clipBehavior: Clip.none,
onClosing: () => null,
builder: (context) => Padding(
padding: EdgeInsets.fromLTRB(5, 0, 5, 0),
child: CharacterCounter()))
: null,
);
}
}
This is what the page looks like at the start:
As I type, it adds additional lines; if it was just the keyboard, it would automatically scroll so that the bottom line (where I'm typing) is always visible. In this case though, I'm typing line 12, which is just off the screen:
The main issue here is that I was basically subtracting the height of the keyboard twice.
The main changes I made were (1) updating the LayoutBuilder
so availableHeight only subtracted the height of the bottom share sheet, instead of also subtracting the keyboard height, and (2) moving ConstrainedBox
outside the SingleChildScrollView
I think the constraints in layout builder already took the keyboardHeight into account, so all I needed to do was also subtract the bottom share sheet.
class TweetDraftPageScaffold extends StatefulWidget {
const TweetDraftPageScaffold({
super.key,
});
@override
State<TweetDraftPageScaffold> createState() => _TweetDraftPageScaffoldState();
}
class _TweetDraftPageScaffoldState extends State<TweetDraftPageScaffold>
with WidgetsBindingObserver {
bool isKeyboardVisible = false;
double keyboardHeight = 0;
final double bottomSheetHeight = 65;
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
final mediaQueryData = MediaQuery.of(context);
final keyboardVisibleHeight = mediaQueryData.viewInsets.bottom;
setState(() {
isKeyboardVisible = keyboardVisibleHeight > 0;
keyboardHeight = keyboardVisibleHeight;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).canvasColor,
appBar: AppBar(
title: const Text(
'Edit Tweet',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
TextButton(
onPressed: () {
context
.read<ThreadEditorBloc>()
.add(const ThreadEditorEvent.saveThread());
context
.read<ThreadWatcherBloc>()
.add(const ThreadWatcherEvent.refresh());
},
child: Text('Save'))
],
),
body: LayoutBuilder(builder: (context, constraints) {
final double availableHeight = math.max(
constraints.maxHeight - (isKeyboardVisible ? bottomSheetHeight : 0),
0,
);
print('availableHeight: $availableHeight');
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: availableHeight),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: // Contents of the widget
),
),
);
}),
bottomSheet: isKeyboardVisible
? BottomSheet(
constraints: BoxConstraints(maxHeight: bottomSheetHeight),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero),
enableDrag: false,
clipBehavior: Clip.none,
onClosing: () => null,
builder: (context) => Padding(
padding: EdgeInsets.fromLTRB(5, 0, 5, 0),
child: CharacterCounter()))
: null,
);
}
}