🎯 问题背景

  • Flutter 官方 issue #121059 和 #32583 中长期存在的键盘与面板切换抖动问题

  • 影响用户体验,特别是聊天类应用

  • 官方多年未解决

💡 解决方案核心

1. 关键配置

dart

Scaffold(
  resizeToAvoidBottomInset: false, // 必须设置为 false
  body: Column(
    children: [
      Expanded(child: ListView.builder(...)),
      _buildInputView(),    // 输入框
      _buildPanelContainer(), // 底部容器
    ],
  ),
)

2. 类型定义

dart

enum PanelType {
  none,      // 收起
  keyboard,  // 键盘
  emoji,     // 表情
  tool,      // 其它工具
}

3. 核心组件使用

dart

ChatBottomPanelContainer<PanelType>(
  controller: controller,
  inputFocusNode: inputFocusNode,
  otherPanelWidget: (type) {
    switch (type) {
      case PanelType.emoji: return _buildEmojiPickerPanel();
      case PanelType.tool: return _buildToolPanel();
      default: return const SizedBox.shrink();
    }
  },
  onPanelTypeChange: (panelType, data) {
    // 处理面板类型变化
  },
)

4. 切换逻辑

dart

// 切换到表情面板
controller.updatePanelType(
  ChatBottomPanelType.other,
  data: PanelType.emoji,
);

// 切换到键盘
controller.updatePanelType(ChatBottomPanelType.keyboard);

🚀 技术亮点

  1. 类型安全: 使用泛型关联外部自定义面板类型

  2. 焦点管理: 通过 inputFocusNode 统一管理键盘焦点

  3. 平滑过渡: 避免了布局重计算导致的抖动

  4. 灵活扩展: 支持多种自定义面板类型

📦 集成方式

yaml

dependencies:
  chat_bottom_container: ^latest_version




——————————————————————————————————————————

 

在移动端聊天应用中,键盘与功能面板(如表情、文件、相册等)的切换体验直接影响用户的使用感受。本文将深入探讨如何在Flutter中实现丝滑的键盘与面板切换效果。

一、核心原理分析

1.1 键盘与面板切换的本质

在Flutter中,键盘与功能面板的切换本质上涉及以下几个关键点:

  1. 布局调整:键盘弹出时,界面需要重新布局

  2. 动画过渡:面板与键盘之间的切换需要平滑动画

  3. 焦点管理:正确处理输入框的焦点状态

  4. 事件处理:处理触摸、滑动等交互事件

1.2 技术挑战

  1. 键盘高度获取:不同设备、不同输入法的键盘高度不同

  2. 平台差异:iOS和Android在键盘行为上有差异

  3. 性能优化:动画的流畅性要求

  4. 状态同步:键盘状态与面板状态的同步

二、基础实现方案

2.1 基础布局结构

dart

class ChatPage extends StatefulWidget {
  @override
  _ChatPageState createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> with SingleTickerProviderStateMixin {
  final TextEditingController _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  bool _showEmojiPanel = false;
  bool _showMorePanel = false;
  double _keyboardHeight = 0;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('聊天'),
      ),
      body: Column(
        children: [
          // 聊天消息列表
          Expanded(
            child: ListView.builder(
              reverse: true,
              itemCount: 20,
              itemBuilder: (context, index) {
                return _buildMessageItem(index);
              },
            ),
          ),
          
          // 输入区域
          _buildInputArea(),
        ],
      ),
    );
  }
  
  Widget _buildInputArea() {
    return Container(
      color: Colors.white,
      child: Column(
        children: [
          // 输入框和发送按钮
          _buildInputBar(),
          // 表情面板或功能面板
          _buildBottomPanel(),
        ],
      ),
    );
  }
}

2.2 键盘高度监听

dart

class _ChatPageState extends State<ChatPage> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _setupKeyboardListeners();
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _focusNode.dispose();
    _textController.dispose();
    super.dispose();
  }
  
  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    // 监听键盘变化
    final viewInsets = WidgetsBinding.instance.window.viewInsets;
    final bottomInset = viewInsets.bottom;
    
    if (bottomInset > 0) {
      // 键盘弹出
      setState(() {
        _keyboardHeight = bottomInset;
        _showEmojiPanel = false;
        _showMorePanel = false;
      });
    } else {
      // 键盘收起
      if (!_showEmojiPanel && !_showMorePanel) {
        setState(() {
          _keyboardHeight = 0;
        });
      }
    }
  }
  
  void _setupKeyboardListeners() {
    // iOS键盘事件监听(可选)
    if (Platform.isIOS) {
      _focusNode.addListener(() {
        if (!_focusNode.hasFocus) {
          // 输入框失去焦点时,可能是点击了表情或更多按钮
          Future.delayed(const Duration(milliseconds: 100), () {
            if (!_showEmojiPanel && !_showMorePanel) {
              setState(() {
                _keyboardHeight = 0;
              });
            }
          });
        }
      });
    }
  }
}

三、进阶实现:使用Overlay和动画

3.1 Overlay方案的优势

使用Overlay可以实现面板在键盘上层显示,避免布局重排带来的跳动感。

dart

class SmoothChatInput extends StatefulWidget {
  @override
  _SmoothChatInputState createState() => _SmoothChatInputState();
}

class _SmoothChatInputState extends State<SmoothChatInput> 
    with SingleTickerProviderStateMixin, WidgetsBindingObserver {
  
  final TextEditingController _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  late AnimationController _panelController;
  late Animation<double> _panelAnimation;
  
  PanelType _currentPanel = PanelType.none;
  double _keyboardHeight = 0;
  OverlayEntry? _panelOverlayEntry;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    
    _panelController = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
    
    _panelAnimation = CurvedAnimation(
      parent: _panelController,
      curve: Curves.fastOutSlowIn,
    );
    
    _setupFocusListeners();
  }
  
  void _setupFocusListeners() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        // 输入框获取焦点,隐藏面板
        _hidePanel();
      }
    });
  }
  
  void _showPanel(PanelType type) {
    if (_currentPanel == type) {
      // 点击当前已显示的按钮,切换回键盘
      _hidePanel();
      _focusNode.requestFocus();
      return;
    }
    
    _currentPanel = type;
    
    // 先收起键盘
    _focusNode.unfocus();
    
    // 延迟显示面板,确保键盘已收起
    Future.delayed(const Duration(milliseconds: 100), () {
      _createPanelOverlay();
      _panelController.forward();
    });
  }
  
  void _hidePanel() {
    if (_currentPanel != PanelType.none) {
      _panelController.reverse().then((_) {
        _removePanelOverlay();
        _currentPanel = PanelType.none;
      });
    }
  }
  
  void _createPanelOverlay() {
    if (_panelOverlayEntry != null) return;
    
    _panelOverlayEntry = OverlayEntry(
      builder: (context) {
        return Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: SizeTransition(
            sizeFactor: _panelAnimation,
            axisAlignment: -1.0,
            child: _buildPanelContent(),
          ),
        );
      },
    );
    
    Overlay.of(context).insert(_panelOverlayEntry!);
  }
  
  Widget _buildPanelContent() {
    switch (_currentPanel) {
      case PanelType.emoji:
        return EmojiPanel(
          onEmojiSelected: (emoji) {
            _textController.text += emoji;
          },
        );
      case PanelType.more:
        return MoreFunctionPanel(
          onFunctionSelected: _handleFunctionSelected,
        );
      default:
        return Container();
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      padding: const EdgeInsets.all(8),
      child: Row(
        children: [
          // 表情按钮
          IconButton(
            icon: Icon(
              _currentPanel == PanelType.emoji 
                ? Icons.keyboard 
                : Icons.emoji_emotions_outlined,
            ),
            onPressed: () => _showPanel(PanelType.emoji),
          ),
          
          // 输入框
          Expanded(
            child: TextField(
              controller: _textController,
              focusNode: _focusNode,
              decoration: InputDecoration(
                hintText: '输入消息...',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 12,
                ),
              ),
              maxLines: 5,
              minLines: 1,
            ),
          ),
          
          // 发送按钮或更多按钮
          if (_textController.text.isNotEmpty)
            IconButton(
              icon: Icon(Icons.send),
              onPressed: _sendMessage,
            )
          else
            IconButton(
              icon: Icon(
                _currentPanel == PanelType.more
                  ? Icons.keyboard
                  : Icons.add_circle_outline,
              ),
              onPressed: () => _showPanel(PanelType.more),
            ),
        ],
      ),
    );
  }
}

3.2 面板组件实现

dart

// 表情面板
class EmojiPanel extends StatelessWidget {
  final ValueChanged<String> onEmojiSelected;
  
  const EmojiPanel({Key? key, required this.onEmojiSelected}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final panelHeight = MediaQuery.of(context).size.height * 0.35;
    
    return Container(
      height: panelHeight,
      color: Colors.grey[100],
      child: Column(
        children: [
          // 表情分类标签
          _buildCategoryTabs(),
          
          // 表情网格
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(12),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 7,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              itemCount: _emojis.length,
              itemBuilder: (context, index) {
                return GestureDetector(
                  onTap: () => onEmojiSelected(_emojis[index]),
                  child: Container(
                    alignment: Alignment.center,
                    child: Text(
                      _emojis[index],
                      style: const TextStyle(fontSize: 24),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildCategoryTabs() {
    return Container(
      height: 48,
      decoration: BoxDecoration(
        border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _buildCategoryTab('😀', true),
          _buildCategoryTab('🚗', false),
          _buildCategoryTab('🏳️', false),
          _buildCategoryTab('⭐', false),
        ],
      ),
    );
  }
  
  final List<String> _emojis = [
    '😀', '😂', '😍', '😎', '😭', '😡', '😱',
    '👍', '👎', '👏', '🙏', '💪', '👀', '👋',
    '❤️', '💔', '💕', '💖', '💯', '🔥', '✨',
    // ... 更多表情
  ];
}

// 更多功能面板
class MoreFunctionPanel extends StatelessWidget {
  final ValueChanged<MoreFunction> onFunctionSelected;
  
  const MoreFunctionPanel({Key? key, required this.onFunctionSelected}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final panelHeight = MediaQuery.of(context).size.height * 0.3;
    
    return Container(
      height: panelHeight,
      color: Colors.grey[100],
      child: GridView.builder(
        padding: const EdgeInsets.all(20),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 4,
          crossAxisSpacing: 20,
          mainAxisSpacing: 20,
        ),
        itemCount: _functions.length,
        itemBuilder: (context, index) {
          final function = _functions[index];
          return GestureDetector(
            onTap: () => onFunctionSelected(function),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  width: 50,
                  height: 50,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(10),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.grey.withOpacity(0.2),
                        blurRadius: 5,
                        spreadRadius: 1,
                      ),
                    ],
                  ),
                  child: Icon(
                    function.icon,
                    color: Colors.blue,
                    size: 28,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  function.label,
                  style: const TextStyle(fontSize: 12),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
  
  final List<MoreFunction> _functions = [
    MoreFunction(Icons.photo, '相册'),
    MoreFunction(Icons.camera_alt, '拍摄'),
    MoreFunction(Icons.file_present, '文件'),
    MoreFunction(Icons.location_on, '位置'),
    MoreFunction(Icons.contact_phone, '联系人'),
    MoreFunction(Icons.voice_chat, '语音输入'),
    MoreFunction(Icons.redo, '转账'),
    MoreFunction(Icons.card_giftcard, '红包'),
  ];
}

四、高级优化方案

4.1 使用KeyboardVisibilityBuilder

dart

class AdvancedChatInput extends StatefulWidget {
  @override
  _AdvancedChatInputState createState() => _AdvancedChatInputState();
}

class _AdvancedChatInputState extends State<AdvancedChatInput> 
    with TickerProviderStateMixin {
  
  final TextEditingController _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  
  late AnimationController _panelController;
  late Animation<double> _panelHeightAnimation;
  
  PanelState _panelState = PanelState.keyboard;
  double _targetPanelHeight = 0;
  
  @override
  void initState() {
    super.initState();
    
    _panelController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    
    _panelHeightAnimation = Tween<double>(
      begin: 0,
      end: 0,
    ).animate(CurvedAnimation(
      parent: _panelController,
      curve: Curves.easeInOut,
    ));
  }
  
  void _togglePanel(PanelType type) {
    final panelHeight = _calculatePanelHeight(type);
    
    if (_panelState == PanelState.keyboard) {
      // 从键盘切换到面板
      _switchFromKeyboardToPanel(type, panelHeight);
    } else if (_panelState == PanelState.panel && _currentPanelType == type) {
      // 当前面板切换到键盘
      _switchFromPanelToKeyboard();
    } else {
      // 从一个面板切换到另一个面板
      _switchBetweenPanels(type, panelHeight);
    }
  }
  
  void _switchFromKeyboardToPanel(PanelType type, double height) {
    _currentPanelType = type;
    _targetPanelHeight = height;
    
    // 先收起键盘
    _focusNode.unfocus();
    
    // 延迟开始面板动画
    Future.delayed(const Duration(milliseconds: 50), () {
      _panelState = PanelState.panel;
      _startPanelAnimation(0, height);
    });
  }
  
  void _switchFromPanelToKeyboard() {
    _panelState = PanelState.keyboard;
    _startPanelAnimation(_targetPanelHeight, 0).then((_) {
      _focusNode.requestFocus();
      _currentPanelType = null;
      _targetPanelHeight = 0;
    });
  }
  
  Future<void> _startPanelAnimation(double from, double to) {
    _panelHeightAnimation = Tween<double>(
      begin: from,
      end: to,
    ).animate(CurvedAnimation(
      parent: _panelController,
      curve: Curves.easeInOut,
    ));
    
    _panelController.value = 0;
    return _panelController.forward();
  }
  
  double _calculatePanelHeight(PanelType type) {
    final screenHeight = MediaQuery.of(context).size.height;
    
    switch (type) {
      case PanelType.emoji:
        return screenHeight * 0.35;
      case PanelType.more:
        return screenHeight * 0.3;
      default:
        return 0;
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return KeyboardVisibilityBuilder(
      builder: (context, isKeyboardVisible) {
        // 键盘状态变化时的处理
        if (isKeyboardVisible && _panelState == PanelState.panel) {
          // 键盘弹出时,如果面板正在显示,则隐藏面板
          _panelState = PanelState.keyboard;
          _panelController.reverse();
        }
        
        return Column(
          children: [
            // 输入栏
            _buildInputBar(),
            
            // 面板区域
            AnimatedBuilder(
              animation: _panelController,
              builder: (context, child) {
                return Container(
                  height: _panelHeightAnimation.value,
                  child: _panelState == PanelState.panel 
                      ? _buildCurrentPanel()
                      : null,
                );
              },
            ),
          ],
        );
      },
    );
  }
}

4.2 使用CustomSingleChildLayout实现弹性布局

dart

class ElasticChatLayout extends StatefulWidget {
  @override
  _ElasticChatLayoutState createState() => _ElasticChatLayoutState();
}

class _ElasticChatLayoutState extends State<ElasticChatLayout> 
    with SingleTickerProviderStateMixin {
  
  late AnimationController _elasticController;
  double _currentPanelHeight = 0;
  double _targetPanelHeight = 0;
  
  @override
  void initState() {
    super.initState();
    
    _elasticController = AnimationController(
      duration: const Duration(milliseconds: 400),
      vsync: this,
    );
    
    // 使用弹性曲线
    _elasticController.addListener(() {
      setState(() {
        // 计算弹性效果的高度
        _currentPanelHeight = _calculateElasticHeight(
          _elasticController.value,
          _targetPanelHeight,
        );
      });
    });
  }
  
  double _calculateElasticHeight(double animationValue, double targetHeight) {
    // 使用弹性公式
    final overshoot = 1.70158; // 弹性系数
    final value = animationValue - 1;
    
    return targetHeight * (value * value * ((overshoot + 1) * value + overshoot) + 1);
  }
  
  void _showPanelWithElasticEffect(double height) {
    _targetPanelHeight = height;
    _elasticController.forward(from: 0);
  }
  
  @override
  Widget build(BuildContext context) {
    return CustomSingleChildLayout(
      delegate: _ChatLayoutDelegate(
        panelHeight: _currentPanelHeight,
        keyboardHeight: MediaQuery.of(context).viewInsets.bottom,
      ),
      child: Column(
        children: [
          // 消息列表
          Expanded(
            child: ListView(
              children: [
                // 消息内容
              ],
            ),
          ),
          
          // 输入区域
          _buildInputArea(),
          
          // 面板
          Container(
            height: _currentPanelHeight,
            child: _buildPanelContent(),
          ),
        ],
      ),
    );
  }
}

class _ChatLayoutDelegate extends SingleChildLayoutDelegate {
  final double panelHeight;
  final double keyboardHeight;
  
  _ChatLayoutDelegate({
    required this.panelHeight,
    required this.keyboardHeight,
  });
  
  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return BoxConstraints(
      maxWidth: constraints.maxWidth,
      maxHeight: constraints.maxHeight,
    );
  }
  
  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // 计算子组件的位置,实现弹性效果
    final contentHeight = size.height - keyboardHeight - panelHeight;
    final availableHeight = contentHeight;
    
    return Offset(0, size.height - childSize.height);
  }
  
  @override
  bool shouldRelayout(covariant _ChatLayoutDelegate oldDelegate) {
    return oldDelegate.panelHeight != panelHeight ||
           oldDelegate.keyboardHeight != keyboardHeight;
  }
}

五、性能优化与最佳实践

5.1 面板内容的懒加载

dart

class LazyLoadPanel extends StatefulWidget {
  final PanelType type;
  
  const LazyLoadPanel({Key? key, required this.type}) : super(key: key);
  
  @override
  _LazyLoadPanelState createState() => _LazyLoadPanelState();
}

class _LazyLoadPanelState extends State<LazyLoadPanel> 
    with AutomaticKeepAliveClientMixin {
  
  bool _isLoaded = false;
  
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    super.build(context);
    
    if (!_isLoaded) {
      // 延迟加载
      return FutureBuilder(
        future: Future.delayed(const Duration(milliseconds: 50)),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              setState(() {
                _isLoaded = true;
              });
            });
          }
          return _buildSkeleton();
        },
      );
    }
    
    return _buildActualContent();
  }
  
  Widget _buildSkeleton() {
    return Container(
      color: Colors.grey[100],
      child: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
  
  Widget _buildActualContent() {
    switch (widget.type) {
      case PanelType.emoji:
        return EmojiPanel(onEmojiSelected: (_) {});
      case PanelType.more:
        return MoreFunctionPanel(onFunctionSelected: (_) {});
      default:
        return Container();
    }
  }
}

5.2 使用ValueNotifier优化状态管理

dart

class ChatInputViewModel {
  final ValueNotifier<PanelState> panelState = 
      ValueNotifier(PanelState.keyboard);
  final ValueNotifier<double> panelHeight = ValueNotifier(0);
  final ValueNotifier<PanelType?> currentPanel = ValueNotifier(null);
  
  final TextEditingController textController = TextEditingController();
  final FocusNode focusNode = FocusNode();
  
  void togglePanel(PanelType type, double panelHeight) {
    if (currentPanel.value == type) {
      // 切换到键盘
      hidePanel();
      focusNode.requestFocus();
    } else {
      // 切换到面板
      showPanel(type, panelHeight);
    }
  }
  
  void showPanel(PanelType type, double height) {
    focusNode.unfocus();
    Future.delayed(const Duration(milliseconds: 50), () {
      currentPanel.value = type;
      panelHeight.value = height;
      panelState.value = PanelState.panel;
    });
  }
  
  void hidePanel() {
    panelState.value = PanelState.keyboard;
    panelHeight.value = 0;
    currentPanel.value = null;
  }
}

class OptimizedChatInput extends StatelessWidget {
  final ChatInputViewModel viewModel;
  
  const OptimizedChatInput({Key? key, required this.viewModel}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<PanelState>(
      valueListenable: viewModel.panelState,
      builder: (context, panelState, child) {
        return Column(
          children: [
            // 输入栏
            _buildInputBar(context),
            
            // 面板区域
            ValueListenableBuilder<double>(
              valueListenable: viewModel.panelHeight,
              builder: (context, height, child) {
                return AnimatedContainer(
                  duration: const Duration(milliseconds: 300),
                  height: height,
                  curve: Curves.easeInOut,
                  child: ValueListenableBuilder<PanelType?>(
                    valueListenable: viewModel.currentPanel,
                    builder: (context, panelType, child) {
                      return _buildPanelContent(panelType);
                    },
                  ),
                );
              },
            ),
          ],
        );
      },
    );
  }
}

六、处理平台差异

6.1 iOS和Android差异处理

dart

class PlatformAwareChatInput extends StatefulWidget {
  @override
  _PlatformAwareChatInputState createState() => _PlatformAwareChatInputState();
}

class _PlatformAwareChatInputState extends State<PlatformAwareChatInput> 
    with WidgetsBindingObserver {
  
  double _keyboardHeight = 0;
  bool _isKeyboardVisible = false;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    
    if (Platform.isIOS) {
      _setupIOSKeyboardListener();
    } else {
      _setupAndroidKeyboardListener();
    }
  }
  
  void _setupIOSKeyboardListener() {
    // iOS需要特殊处理
    _focusNode.addListener(() {
      if (!_focusNode.hasFocus) {
        // iOS键盘收起动画较慢,需要延迟处理
        Future.delayed(const Duration(milliseconds: 200), () {
          if (mounted) {
            setState(() {
              _isKeyboardVisible = false;
              _keyboardHeight = 0;
            });
          }
        });
      }
    });
  }
  
  void _setupAndroidKeyboardListener() {
    // Android处理相对简单
  }
  
  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    
    final viewInsets = WidgetsBinding.instance.window.viewInsets;
    final newKeyboardHeight = viewInsets.bottom;
    
    if (Platform.isIOS) {
      _handleIOSKeyboardChange(newKeyboardHeight);
    } else {
      _handleAndroidKeyboardChange(newKeyboardHeight);
    }
  }
  
  void _handleIOSKeyboardChange(double newKeyboardHeight) {
    // iOS键盘高度变化处理
    if (newKeyboardHeight > 0 && !_isKeyboardVisible) {
      _isKeyboardVisible = true;
      _keyboardHeight = newKeyboardHeight;
    } else if (newKeyboardHeight == 0 && _isKeyboardVisible) {
      // 在iOS上,键盘收起可能不是立即的
      // 我们通过焦点监听来处理
    }
  }
  
  void _handleAndroidKeyboardChange(double newKeyboardHeight) {
    // Android键盘高度变化处理
    setState(() {
      _keyboardHeight = newKeyboardHeight;
      _isKeyboardVisible = newKeyboardHeight > 0;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: Platform.isIOS ? false : true,
      body: SafeArea(
        bottom: Platform.isAndroid,
        child: _buildChatLayout(),
      ),
    );
  }
}

七、完整示例代码

dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

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

class ChatApp extends StatelessWidget {
  const ChatApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 聊天',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        platform: TargetPlatform.iOS,
      ),
      home: const CompleteChatPage(),
    );
  }
}

class CompleteChatPage extends StatefulWidget {
  const CompleteChatPage({Key? key}) : super(key: key);

  @override
  _CompleteChatPageState createState() => _CompleteChatPageState();
}

class _CompleteChatPageState extends State<CompleteChatPage> 
    with SingleTickerProviderStateMixin, WidgetsBindingObserver {
  
  final TextEditingController _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  
  late AnimationController _panelController;
  late Animation<double> _panelHeightAnimation;
  
  PanelType? _currentPanel;
  double _keyboardHeight = 0;
  bool _isKeyboardVisible = false;
  
  final double _emojiPanelHeight = 280;
  final double _morePanelHeight = 220;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    
    _panelController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    
    _panelHeightAnimation = Tween<double>(begin: 0, end: 0).animate(
      CurvedAnimation(
        parent: _panelController,
        curve: Curves.easeInOut,
      ),
    );
    
    _setupKeyboardListeners();
  }
  
  void _setupKeyboardListeners() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus && _currentPanel != null) {
        // 输入框获取焦点时,如果有面板显示,则隐藏面板
        _hidePanel();
      }
    });
  }
  
  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    
    final viewInsets = WidgetsBinding.instance.window.viewInsets;
    final bottomInset = viewInsets.bottom;
    
    if (bottomInset > 0) {
      // 键盘弹出
      _isKeyboardVisible = true;
      _keyboardHeight = bottomInset;
      
      if (_currentPanel != null) {
        // 键盘弹出时隐藏面板
        _hidePanel();
      }
    } else if (_isKeyboardVisible) {
      // 键盘收起
      _isKeyboardVisible = false;
      _keyboardHeight = 0;
    }
  }
  
  void _togglePanel(PanelType type) {
    if (_currentPanel == type) {
      // 切换到键盘
      _hidePanel();
      _focusNode.requestFocus();
    } else {
      // 切换到面板
      _showPanel(type);
    }
  }
  
  void _showPanel(PanelType type) {
    _currentPanel = type;
    
    // 先收起键盘
    _focusNode.unfocus();
    
    // 延迟显示面板
    Future.delayed(const Duration(milliseconds: 100), () {
      if (mounted) {
        final targetHeight = type == PanelType.emoji 
            ? _emojiPanelHeight 
            : _morePanelHeight;
        
        _panelHeightAnimation = Tween<double>(
          begin: 0,
          end: targetHeight,
        ).animate(
          CurvedAnimation(
            parent: _panelController,
            curve: Curves.easeInOut,
          ),
        );
        
        _panelController.forward(from: 0);
      }
    });
  }
  
  void _hidePanel() {
    if (_currentPanel != null) {
      _panelController.reverse().then((_) {
        if (mounted) {
          setState(() {
            _currentPanel = null;
          });
        }
      });
    }
  }
  
  void _sendMessage() {
    final text = _textController.text.trim();
    if (text.isNotEmpty) {
      // 发送消息逻辑
      _textController.clear();
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('聊天'),
        elevation: 1,
      ),
      body: GestureDetector(
        onTap: () {
          // 点击空白处收起键盘和面板
          _focusNode.unfocus();
          _hidePanel();
        },
        child: Column(
          children: [
            // 消息列表
            Expanded(
              child: ListView.builder(
                reverse: true,
                padding: const EdgeInsets.all(16),
                itemCount: 20,
                itemBuilder: (context, index) {
                  return _buildMessageBubble(index);
                },
              ),
            ),
            
            // 输入区域
            _buildInputArea(),
            
            // 面板区域
            AnimatedBuilder(
              animation: _panelController,
              builder: (context, child) {
                return Container(
                  height: _panelHeightAnimation.value,
                  child: _currentPanel != null 
                      ? _buildPanelContent(_currentPanel!)
                      : null,
                );
              },
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildInputArea() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(top: BorderSide(color: Colors.grey.shade300)),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          // 语音/表情切换按钮
          IconButton(
            icon: Icon(
              _currentPanel == PanelType.emoji
                  ? Icons.keyboard
                  : Icons.emoji_emotions_outlined,
              color: Colors.grey.shade600,
            ),
            onPressed: () => _togglePanel(PanelType.emoji),
          ),
          
          // 输入框
          Expanded(
            child: Container(
              constraints: const BoxConstraints(
                maxHeight: 120,
              ),
              child: TextField(
                controller: _textController,
                focusNode: _focusNode,
                maxLines: null,
                textInputAction: TextInputAction.send,
                onSubmitted: (_) => _sendMessage(),
                decoration: InputDecoration(
                  hintText: '输入消息...',
                  filled: true,
                  fillColor: Colors.grey.shade100,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(20),
                    borderSide: BorderSide.none,
                  ),
                  contentPadding: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 12,
                  ),
                ),
              ),
            ),
          ),
          
          const SizedBox(width: 8),
          
          // 发送/更多切换按钮
          if (_textController.text.trim().isNotEmpty)
            IconButton(
              icon: const Icon(Icons.send, color: Colors.blue),
              onPressed: _sendMessage,
            )
          else
            IconButton(
              icon: Icon(
                _currentPanel == PanelType.more
                    ? Icons.keyboard
                    : Icons.add_circle_outline,
                color: Colors.grey.shade600,
              ),
              onPressed: () => _togglePanel(PanelType.more),
            ),
        ],
      ),
    );
  }
  
  Widget _buildPanelContent(PanelType type) {
    switch (type) {
      case PanelType.emoji:
        return _buildEmojiPanel();
      case PanelType.more:
        return _buildMorePanel();
    }
  }
  
  Widget _buildEmojiPanel() {
    const emojis = [
      '😀', '😂', '😍', '😎', '😭', '😡', '😱',
      '👍', '👎', '👏', '🙏', '💪', '👀', '👋',
      '❤️', '💔', '💕', '💖', '💯', '🔥', '✨',
    ];
    
    return Container(
      color: Colors.grey.shade50,
      child: Column(
        children: [
          // 表情分类
          Container(
            height: 40,
            decoration: BoxDecoration(
              border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
            ),
            child: Row(
              children: [
                _buildEmojiCategory('最近', isSelected: true),
                _buildEmojiCategory('表情'),
                _buildEmojiCategory('手势'),
                _buildEmojiCategory('物品'),
              ],
            ),
          ),
          
          // 表情网格
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(12),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 7,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              itemCount: emojis.length,
              itemBuilder: (context, index) {
                return GestureDetector(
                  onTap: () {
                    _textController.text += emojis[index];
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(8),
                      color: Colors.white,
                    ),
                    alignment: Alignment.center,
                    child: Text(
                      emojis[index],
                      style: const TextStyle(fontSize: 24),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildEmojiCategory(String title, {bool isSelected = false}) {
    return Expanded(
      child: Container(
        alignment: Alignment.center,
        decoration: BoxDecoration(
          border: isSelected
              ? Border(bottom: BorderSide(color: Colors.blue, width: 2))
              : null,
        ),
        child: Text(
          title,
          style: TextStyle(
            color: isSelected ? Colors.blue : Colors.grey.shade600,
            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }
  
  Widget _buildMorePanel() {
    final functions = [
      _MoreFunction(Icons.photo, '相册'),
      _MoreFunction(Icons.camera_alt, '拍摄'),
      _MoreFunction(Icons.file_present, '文件'),
      _MoreFunction(Icons.location_on, '位置'),
      _MoreFunction(Icons.contact_phone, '联系人'),
      _MoreFunction(Icons.voice_chat, '语音输入'),
    ];
    
    return Container(
      color: Colors.grey.shade50,
      padding: const EdgeInsets.all(16),
      child: GridView.builder(
        physics: const NeverScrollableScrollPhysics(),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 4,
          crossAxisSpacing: 16,
          mainAxisSpacing: 16,
        ),
        itemCount: functions.length,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              // 处理功能点击
              _handleFunctionClick(functions[index].label);
            },
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  width: 50,
                  height: 50,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(12),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.grey.withOpacity(0.1),
                        blurRadius: 6,
                        spreadRadius: 2,
                      ),
                    ],
                  ),
                  child: Icon(
                    functions[index].icon,
                    color: Colors.blue,
                    size: 26,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  functions[index].label,
                  style: const TextStyle(fontSize: 12, color: Colors.grey),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
  
  void _handleFunctionClick(String label) {
    // 处理功能点击
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('点击了: $label')),
    );
  }
  
  Widget _buildMessageBubble(int index) {
    final isMe = index % 3 == 0;
    
    return Align(
      alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        constraints: BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width * 0.7,
        ),
        margin: const EdgeInsets.only(bottom: 8),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        decoration: BoxDecoration(
          color: isMe ? Colors.blue.shade100 : Colors.grey.shade200,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          '消息 $index ${isMe ? '(我)' : ''}',
          style: const TextStyle(fontSize: 16),
        ),
      ),
    );
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _focusNode.dispose();
    _textController.dispose();
    _panelController.dispose();
    super.dispose();
  }
}

enum PanelType { emoji, more }

class _MoreFunction {
  final IconData icon;
  final String label;
  
  _MoreFunction(this.icon, this.label);
}

八、常见问题与解决方案

8.1 键盘跳动问题

问题描述:面板与键盘切换时出现跳动

解决方案

dart

// 使用键盘监听和动画同步
void _switchPanel(PanelType type) {
  if (_isKeyboardVisible) {
    // 键盘正在显示,先收起键盘
    _focusNode.unfocus();
    
    // 等待键盘动画完成
    Future.delayed(const Duration(milliseconds: 200), () {
      _showPanelWithAnimation(type);
    });
  } else {
    _showPanelWithAnimation(type);
  }
}

8.2 输入框遮挡问题

解决方案

dart

// 使用ListView.builder的reverse参数
ListView.builder(
  reverse: true, // 重要:反向列表
  itemCount: messages.length,
  itemBuilder: (context, index) {
    return MessageItem(messages[index]);
  },
)

8.3 内存泄漏问题

解决方案

dart

@override
void dispose() {
  // 清理所有监听器和控制器
  _focusNode.dispose();
  _textController.dispose();
  _panelController.dispose();
  WidgetsBinding.instance.removeObserver(this);
  super.dispose();
}

九、测试与调试

9.1 键盘测试工具

dart

class KeyboardTestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Expanded(child: Container()),
          TextField(
            decoration: InputDecoration(hintText: '测试输入框'),
          ),
          ElevatedButton(
            onPressed: () {
              // 打印键盘信息
              final viewInsets = MediaQuery.of(context).viewInsets;
              print('键盘高度: ${viewInsets.bottom}');
            },
            child: Text('检查键盘高度'),
          ),
        ],
      ),
    );
  }
}

9.2 性能监控

使用Flutter DevTools的Performance面板监控动画性能,确保帧率保持在60fps以上。

十、总结

实现Flutter中聊天键盘与功能面板的丝滑切换需要综合考虑以下几个方面:

  1. 布局策略:选择合适的布局方式(Overlay或内嵌)

  2. 动画控制:使用合适的动画曲线和持续时间

  3. 状态管理:正确处理键盘状态和面板状态的同步

  4. 性能优化:懒加载、缓存、避免不必要的重建

  5. 平台适配:处理iOS和Android的差异

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐