最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

flutter - FloatingActionButton (FAB) moves up when the keyboard opens - Stack Overflow

programmeradmin3浏览0评论

I'm trying to keep the FloatingActionButton (FAB) fixed at the bottom when the keyboard opens in my Flutter app, but it keeps moving up when the keyboard appears.

class RecentDmConversationsPageBody extends StatefulWidget {
  const RecentDmConversationsPageBody({super.key});
  @override
  State<RecentDmConversationsPageBody> createState() => _RecentDmConversationsPageBodyState();
}
class _RecentDmConversationsPageBodyState extends State<RecentDmConversationsPageBody> with PerAccountStoreAwareStateMixin<RecentDmConversationsPageBody>{
  RecentDmConversationsView? model;
  Unreads? unreadsModel;
  final TextEditingController _searchController = TextEditingController();
  List<DmNarrow> _filteredConversations = [];
  bool _isSearching = false;
  @override
  void onNewStore() {
    model?.removeListener(_modelChanged);
    model = PerAccountStoreWidget.of(context).recentDmConversationsView
      ..addListener(_modelChanged);
    unreadsModel?.removeListener(_modelChanged);
    unreadsModel = PerAccountStoreWidget.of(context).unreads
      ..addListener(_modelChanged);
    _applySearchFilter();
  }
  @override
  void initState() {
    super.initState();
    _searchController.addListener(_applySearchFilter);
  }
  @override
  void dispose() {
    model?.removeListener(_modelChanged);
    unreadsModel?.removeListener(_modelChanged);
    _searchController.dispose();
    super.dispose();
  }
  void _modelChanged() {
    setState(() {
      // The actual state lives in [model] and [unreadsModel].
      // This method was called because one of those just changed.      _applySearchFilter();
    });
  }
  void _applySearchFilter() {
    final query = _searchController.text.toLowerCase();
    if (query.isEmpty) {
      _filteredConversations = List.from(model!.sorted);
    } else {
      _filteredConversations = model!.sorted.where((narrow) {
        final store = PerAccountStoreWidget.of(context);
        final selfUser = store.users[store.selfUserId]!;
        final otherRecipientIds = narrow.otherRecipientIds;
        if (otherRecipientIds.isEmpty) {
          return selfUser.fullName.toLowerCase().contains(query);
        } else {
          return otherRecipientIds.any((id) {
            final user = store.users[id];
            return user?.fullName.toLowerCase().contains(query) ?? false;
          });
        }
      }).toList();
    }
    setState(() {
      _isSearching = query.isNotEmpty;
    });
  }
  @override
  Widget build(BuildContext context) {
    // Check if there are any DMs at all in the original model
    if (model!.sorted.isEmpty) {
      return const EmptyDmState();
    }
    return Scaffold(
      backgroundColor: DesignVariables.of(context).mainBackground,
      resizeToAvoidBottomInset: true,
      body: SafeArea(
        // Don't pad the bottom here; we want the list content to do that.
          bottom: false,
          child: Stack(
            children: [Column(
              children: [
                SearchRow(controller: _searchController),
                Expanded(
                  child: ListView.builder(
                      itemCount: _filteredConversations.length + (_isSearching ? 1 : 0),
                      itemBuilder: (context, index) {
                        if(index < _filteredConversations.length) {
                          final narrow = _filteredConversations[index];
                          return RecentDmConversationsItem(
                            narrow: narrow,
                            unreadCount: unreadsModel!.countInDmNarrow(narrow),
                            searchQuery: _searchController.text,
                          );
                        }
                        else{
                          return const NewDirectMessageButton();
                        }
                      }),
                )
              ],
            ),
            ],
          )),
      floatingActionButton: Visibility(
        visible: !_isSearching,
        child: const NewDmButton(),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    );
  }
}

class NewDirectMessageButton extends StatelessWidget {
  const NewDirectMessageButton({super.key});
  @override
  Widget build(BuildContext context) {
    final designVariables = DesignVariables.of(context);
    return Container(
      margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12), // Match the button's shape
        color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity
      ),
      child: FilledButton.icon(
        style: FilledButton.styleFrom(
          minimumSize: const Size(137, 44),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          backgroundColor: Colors.transparent,
        ),
        onPressed: (){
          Navigator.of(context).push(
              NewDmScreen.buildRoute(context: context)
          );
        },
        icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24),
        label: Text(
          'New Direct Message',
          style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600),
        ),
      ),
    );
  }
}
class NewDmButton extends StatelessWidget {
  const NewDmButton({super.key});
  @override
  Widget build(BuildContext context) {
    final designVariables = DesignVariables.of(context);
    return Container(
      padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16),
      decoration: BoxDecoration(
        boxShadow: const [
          BoxShadow(
            color: Color(0x662B0E8A), // 40% opacity for #2B0E8A
            offset: Offset(0, 4), // X: 0, Y: 4
            blurRadius: 16, // Blur: 16
            spreadRadius: 0, // Spread: 0
          ),
        ],
        borderRadius: BorderRadius.circular(28), // Match the button's shape
      ),
      child: FilledButton.icon(
        style: FilledButton.styleFrom(
          minimumSize: const Size(137, 48),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(28),
          ),
          backgroundColor: designVariables.fabBg,
        ),
        onPressed: (){
          Navigator.of(context).push(
              NewDmScreen.buildRoute(context: context)
          );
        },
        icon: const Icon(Icons.add, color: Colors.white, size: 24),
        label: Text(
          'New DM',
          style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500),
        ),
      ),
    );
  }
}

What I Have Tried:

  1. Set resizeToAvoidBottomInset: false in Scaffold.

    • This prevents the body from resizing, but the FAB still moves.
  2. Used Stack with Positioned outside the Scaffold.

    • The FAB still shifts when the keyboard opens.
  3. Checked for keyboard visibility using MediaQuery.of(context).viewInsets.bottom inside a Visibility widget.

    • While this hides the FAB when the keyboard is open, it does not prevent the movement when visible.

Expected Behavior:

  • The FAB should always remain at the bottom of the screen, even when the keyboard opens.

  • The keyboard should not push the FAB up.

I'm trying to keep the FloatingActionButton (FAB) fixed at the bottom when the keyboard opens in my Flutter app, but it keeps moving up when the keyboard appears.

class RecentDmConversationsPageBody extends StatefulWidget {
  const RecentDmConversationsPageBody({super.key});
  @override
  State<RecentDmConversationsPageBody> createState() => _RecentDmConversationsPageBodyState();
}
class _RecentDmConversationsPageBodyState extends State<RecentDmConversationsPageBody> with PerAccountStoreAwareStateMixin<RecentDmConversationsPageBody>{
  RecentDmConversationsView? model;
  Unreads? unreadsModel;
  final TextEditingController _searchController = TextEditingController();
  List<DmNarrow> _filteredConversations = [];
  bool _isSearching = false;
  @override
  void onNewStore() {
    model?.removeListener(_modelChanged);
    model = PerAccountStoreWidget.of(context).recentDmConversationsView
      ..addListener(_modelChanged);
    unreadsModel?.removeListener(_modelChanged);
    unreadsModel = PerAccountStoreWidget.of(context).unreads
      ..addListener(_modelChanged);
    _applySearchFilter();
  }
  @override
  void initState() {
    super.initState();
    _searchController.addListener(_applySearchFilter);
  }
  @override
  void dispose() {
    model?.removeListener(_modelChanged);
    unreadsModel?.removeListener(_modelChanged);
    _searchController.dispose();
    super.dispose();
  }
  void _modelChanged() {
    setState(() {
      // The actual state lives in [model] and [unreadsModel].
      // This method was called because one of those just changed.      _applySearchFilter();
    });
  }
  void _applySearchFilter() {
    final query = _searchController.text.toLowerCase();
    if (query.isEmpty) {
      _filteredConversations = List.from(model!.sorted);
    } else {
      _filteredConversations = model!.sorted.where((narrow) {
        final store = PerAccountStoreWidget.of(context);
        final selfUser = store.users[store.selfUserId]!;
        final otherRecipientIds = narrow.otherRecipientIds;
        if (otherRecipientIds.isEmpty) {
          return selfUser.fullName.toLowerCase().contains(query);
        } else {
          return otherRecipientIds.any((id) {
            final user = store.users[id];
            return user?.fullName.toLowerCase().contains(query) ?? false;
          });
        }
      }).toList();
    }
    setState(() {
      _isSearching = query.isNotEmpty;
    });
  }
  @override
  Widget build(BuildContext context) {
    // Check if there are any DMs at all in the original model
    if (model!.sorted.isEmpty) {
      return const EmptyDmState();
    }
    return Scaffold(
      backgroundColor: DesignVariables.of(context).mainBackground,
      resizeToAvoidBottomInset: true,
      body: SafeArea(
        // Don't pad the bottom here; we want the list content to do that.
          bottom: false,
          child: Stack(
            children: [Column(
              children: [
                SearchRow(controller: _searchController),
                Expanded(
                  child: ListView.builder(
                      itemCount: _filteredConversations.length + (_isSearching ? 1 : 0),
                      itemBuilder: (context, index) {
                        if(index < _filteredConversations.length) {
                          final narrow = _filteredConversations[index];
                          return RecentDmConversationsItem(
                            narrow: narrow,
                            unreadCount: unreadsModel!.countInDmNarrow(narrow),
                            searchQuery: _searchController.text,
                          );
                        }
                        else{
                          return const NewDirectMessageButton();
                        }
                      }),
                )
              ],
            ),
            ],
          )),
      floatingActionButton: Visibility(
        visible: !_isSearching,
        child: const NewDmButton(),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    );
  }
}

class NewDirectMessageButton extends StatelessWidget {
  const NewDirectMessageButton({super.key});
  @override
  Widget build(BuildContext context) {
    final designVariables = DesignVariables.of(context);
    return Container(
      margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12), // Match the button's shape
        color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity
      ),
      child: FilledButton.icon(
        style: FilledButton.styleFrom(
          minimumSize: const Size(137, 44),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          backgroundColor: Colors.transparent,
        ),
        onPressed: (){
          Navigator.of(context).push(
              NewDmScreen.buildRoute(context: context)
          );
        },
        icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24),
        label: Text(
          'New Direct Message',
          style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600),
        ),
      ),
    );
  }
}
class NewDmButton extends StatelessWidget {
  const NewDmButton({super.key});
  @override
  Widget build(BuildContext context) {
    final designVariables = DesignVariables.of(context);
    return Container(
      padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16),
      decoration: BoxDecoration(
        boxShadow: const [
          BoxShadow(
            color: Color(0x662B0E8A), // 40% opacity for #2B0E8A
            offset: Offset(0, 4), // X: 0, Y: 4
            blurRadius: 16, // Blur: 16
            spreadRadius: 0, // Spread: 0
          ),
        ],
        borderRadius: BorderRadius.circular(28), // Match the button's shape
      ),
      child: FilledButton.icon(
        style: FilledButton.styleFrom(
          minimumSize: const Size(137, 48),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(28),
          ),
          backgroundColor: designVariables.fabBg,
        ),
        onPressed: (){
          Navigator.of(context).push(
              NewDmScreen.buildRoute(context: context)
          );
        },
        icon: const Icon(Icons.add, color: Colors.white, size: 24),
        label: Text(
          'New DM',
          style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500),
        ),
      ),
    );
  }
}

What I Have Tried:

  1. Set resizeToAvoidBottomInset: false in Scaffold.

    • This prevents the body from resizing, but the FAB still moves.
  2. Used Stack with Positioned outside the Scaffold.

    • The FAB still shifts when the keyboard opens.
  3. Checked for keyboard visibility using MediaQuery.of(context).viewInsets.bottom inside a Visibility widget.

    • While this hides the FAB when the keyboard is open, it does not prevent the movement when visible.

Expected Behavior:

  • The FAB should always remain at the bottom of the screen, even when the keyboard opens.

  • The keyboard should not push the FAB up.

Share Improve this question edited Feb 17 at 6:34 DarkBee 15.6k8 gold badges72 silver badges116 bronze badges asked Feb 17 at 1:31 Dhanesh SawantDhanesh Sawant 311 silver badge1 bronze badge 1
  • 2 resizeToAvoidBottomInset: false shouldn't move the fab, Can you simplify the snippet and share minimal code-snippet that can be run in dartPad – Md. Yeasin Sheikh Commented Feb 17 at 3:03
Add a comment  | 

2 Answers 2

Reset to default 1

I have faced same issue on my side as well and I apply below code and its working well when keyboard is open then float button not goes up.

floatingActionButton: Visibility(
              visible: MediaQuery.of(context).viewInsets.bottom == 0.0,
              child: Container(),// add your widget here
            ),
            floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

Here is a minimal code example with your expected behavior of not pushing the FAB when the keyboard is opening/opened.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: const Example(),
    );
  }
}

class Example extends StatelessWidget {
  const Example({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: Scaffold(
        body: GestureDetector(
          onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: TextField(
                    decoration: const InputDecoration(border: OutlineInputBorder(), labelText: 'Enter your username'),
                  ),
                ),
              ],
            ),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            print("Hello World");
          },
          child: const Icon(Icons.add),
        ),
        // This does the magic.
        resizeToAvoidBottomInset: false,
      ),
    );
  }
}

The clue is to use resizeToAvoidBottomInset: false on the Scaffold of which the FAB is attached to.

Please reconsider testing it and if needed, update your Flutter Version.

Also update your code and maybe attach a video/gif next time!

发布评论

评论列表(0)

  1. 暂无评论