You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter TV应用:如何将键盘显示在屏幕左侧

TV端Flutter应用键盘遮挡输入框及位置调整方案

问题现状

在TV端Flutter应用中,唤起键盘时输入框会被遮挡。尝试通过SingleChildScrollView结合键盘高度设置内边距的方案无效,当前效果如下:
输入框被键盘遮挡

期望将键盘移动到屏幕左端,避免遮挡输入框,目标效果如下:
键盘位于屏幕左端

解决方案

针对TV端的键盘问题,从布局优化和键盘适配两个方向处理:

  1. 修复滚动视图失效问题:移除固定高度限制,让SingleChildScrollView真正实现滚动;
  2. 动态适配键盘高度:监听键盘弹出事件,调整页面底部内边距;
  3. 键盘位置调整:部分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;
                              },
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

关键调整说明

  1. 滚动视图修复:将原代码中Container的固定height: sHeight改为minHeight: sHeight,让容器自适应内容高度,确保SingleChildScrollView可以正常滚动;
  2. 动态内边距:通过MediaQuery监听键盘高度,动态设置SingleChildScrollView的底部内边距,避免输入框被遮挡;
  3. 键盘位置适配:若需要强制将键盘移到左侧,可针对Android TV平台修改原生配置,或使用第三方TV键盘插件自定义布局(部分系统默认键盘位置无法通过Flutter直接修改)。

内容的提问来源于stack exchange,提问作者Rajasekaran K

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 13:25:54