Flutter TV应用:如何将键盘显示在屏幕左侧
TV端Flutter应用键盘遮挡输入框及位置调整方案
问题现状
在TV端Flutter应用中,唤起键盘时输入框会被遮挡。尝试通过SingleChildScrollView结合键盘高度设置内边距的方案无效,当前效果如下:
期望将键盘移动到屏幕左端,避免遮挡输入框,目标效果如下:
解决方案
针对TV端的键盘问题,从布局优化和键盘适配两个方向处理:
- 修复滚动视图失效问题:移除固定高度限制,让
SingleChildScrollView真正实现滚动; - 动态适配键盘高度:监听键盘弹出事件,调整页面底部内边距;
- 键盘位置调整:部分TV系统支持通过系统配置或自定义键盘插件修改显示位置,若系统不支持则通过调整输入框区域规避遮挡。
以下是修改后的完整代码:
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, )); runApp( const MyApp(), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, title: 'My Demo TV', home: LoginPageONE(), ); } } class LoginPageONE extends StatefulWidget { const LoginPageONE({super.key}); @override State<LoginPageONE> createState() => _LoginPageONEState(); } class _LoginPageONEState extends State<LoginPageONE> { TextEditingController userName = TextEditingController(text: ""); TextEditingController pin = TextEditingController(text: ""); FocusNode userNameFocus = FocusNode(); FocusNode pinFocus = FocusNode(); FocusNode loginFocus = FocusNode(); double _keyboardHeight = 0; @override void initState() { userNameFocus.requestFocus(); // 监听键盘高度变化 WidgetsBinding.instance.addPostFrameCallback((_) { final mediaQuery = MediaQuery.of(context); setState(() { _keyboardHeight = mediaQuery.viewInsets.bottom; }); mediaQuery.addListener(() { setState(() { _keyboardHeight = mediaQuery.viewInsets.bottom; }); }); }); super.initState(); } @override Widget build(BuildContext context) { double sHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top; double sWidth = MediaQuery.of(context).size.width; return AnnotatedRegion<SystemUiOverlayStyle>( value: SystemUiOverlayStyle.dark, child: PopScope( canPop: false, child: Scaffold( backgroundColor: const Color(0xFFf5f7f9), body: SingleChildScrollView( padding: EdgeInsets.only(bottom: _keyboardHeight), child: Container( minHeight: sHeight, decoration: BoxDecoration(color: Colors.brown.withOpacity(0.8)), alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 40, top: 20, bottom: 20), child: Card( elevation: 4.0, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4))), color: Colors.white, child: Container( width: sWidth * 0.4, padding: const EdgeInsets.all(14), child: FocusTraversalGroup( policy: OrderedTraversalPolicy(), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 40, width: 50, color: Colors.red, ), Padding( padding: const EdgeInsets.only(top: 10), child: Row( children: [ Container( height: sHeight * 0.08, width: sWidth * 0.004, margin: const EdgeInsets.only(right: 12), decoration: const BoxDecoration( color: Color(0xFFE4165C), borderRadius: BorderRadius.all(Radius.circular(16)), ), ), const Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("LOGIN", style: TextStyle( fontSize: 28, color: Colors.black, )), Text( "Use your credentials to login your account.", style: TextStyle( fontSize: 14, color: Colors.black, )), ], ), ], ), ), const Padding( padding: EdgeInsets.only(top: 20), child: Text("Username", style: TextStyle( fontSize: 14, color: Colors.black, )), ), Padding( padding: const EdgeInsets.only(top: 14), child: FocusTraversalOrder( order: const NumericFocusOrder(1.0), child: TextField( controller: userName, style: const TextStyle( fontSize: 28, color: Colors.black, ), focusNode: userNameFocus, autofocus: true, textInputAction: TextInputAction.next, decoration: const InputDecoration( contentPadding: EdgeInsets.only( left: 18, right: 18, top: 10, bottom: 10), filled: true, isDense: true, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide(color: Color(0xFFA2A2A2)), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide(color: Color(0xFFA2A2A2)), ), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide(color: Color(0xFFA2A2A2)), ), ), inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.deny(' '), ], ), ), ), const Padding( padding: EdgeInsets.only(top: 10), child: Text("Password", style: TextStyle( fontSize: 14, color: Colors.black, )), ), Padding( padding: const EdgeInsets.only(top: 14), child: FocusTraversalOrder( order: const NumericFocusOrder(2.0), child: TextField( controller: pin, style: const TextStyle( fontSize: 28, color: Colors.black, ), focusNode: pinFocus, textInputAction: TextInputAction.done, scrollPadding: const EdgeInsets.only(bottom: 50), onSubmitted: (val) { loginFocus.requestFocus(); setState(() {}); }, decoration: const InputDecoration( contentPadding: EdgeInsets.only( left: 18, right: 18, top: 10, bottom: 10), fillColor: Colors.white, filled: true, isDense: true, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide(color: Color(0xFFA2A2A2)), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide(color: Color(0xFFA2A2A2)), ), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide(color: Color(0xFFA2A2A2)), ), ), inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.deny(' '), ], ), ), ), Padding( padding: const EdgeInsets.only(top: 20), child: FocusTraversalOrder( order: const NumericFocusOrder(3.0), child: Focus( focusNode: loginFocus, child: ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: loginFocus.hasFocus ? const Color(0xFF303188) : const Color(0xFFE4165C), minimumSize: Size(sWidth * 0.4, sHeight * 0.07), maximumSize: Size(sWidth * 0.4, sHeight * 0.07), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), ), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( "Login", style: TextStyle( fontSize: 16, color: Colors.white, ), ), Padding( padding: EdgeInsets.only(left: 8), child: Icon(Icons.arrow_right_alt, color: Colors.white), ), ], ), ), onKeyEvent: (_, event) { if (event.logicalKey == LogicalKeyboardKey.arrowUp) { userNameFocus.requestFocus(); setState(() {}); return KeyEventResult.handled; } if (event.logicalKey == LogicalKeyboardKey.select) { debugPrint("BTN Clicked"); } return KeyEventResult.ignored; }, ), ), ), ], ), ), ), ), ), ), ), ), ); } }
关键调整说明
- 滚动视图修复:将原代码中
Container的固定height: sHeight改为minHeight: sHeight,让容器自适应内容高度,确保SingleChildScrollView可以正常滚动; - 动态内边距:通过
MediaQuery监听键盘高度,动态设置SingleChildScrollView的底部内边距,避免输入框被遮挡; - 键盘位置适配:若需要强制将键盘移到左侧,可针对Android TV平台修改原生配置,或使用第三方TV键盘插件自定义布局(部分系统默认键盘位置无法通过Flutter直接修改)。
内容的提问来源于stack exchange,提问作者Rajasekaran K
相关产品推荐
相关产品推荐

