Search code examples
flutterstatefulwidget

Make widget take up a little less than available space (to account for widget above the keyboard)


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,
    );
  }
}

Images

This is what the page looks like at the start:

enter image description here

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:

enter image description here


Solution

  • 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,
        );
      }
    }