custom_tv_text_field 1.0.0 copy "custom_tv_text_field: ^1.0.0" to clipboard
custom_tv_text_field: ^1.0.0 copied to clipboard

A premium, TV-optimized custom text field and keyboard for Flutter applications. Support for remote control navigation, automated validation, and smooth animations.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TV Keyboard Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.dark,
          surface: const Color(0xFF121212),
        ),
        scaffoldBackgroundColor: const Color(0xFF121212),
        useMaterial3: true,
      ),
      home: const TVLoginScreen(),
    );
  }
}

enum LoginSection { email, password, phone, loginButton }

class TVLoginScreen extends StatefulWidget {
  const TVLoginScreen({super.key});

  @override
  State<TVLoginScreen> createState() => _TVLoginScreenState();
}

class _TVLoginScreenState extends State<TVLoginScreen> {
  final FocusNode screenFocusNode = FocusNode();
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final ValueNotifier<LoginSection> _currentSection =
      ValueNotifier<LoginSection>(LoginSection.email);
  final ValueNotifier<bool> _hasKeyboardOpen = ValueNotifier<bool>(false);

  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  final TextEditingController phoneController = TextEditingController();

  final GlobalKey<CustomTVTextFieldState> emailKey =
      GlobalKey<CustomTVTextFieldState>();
  final GlobalKey<CustomTVTextFieldState> passwordKey =
      GlobalKey<CustomTVTextFieldState>();
  final GlobalKey<CustomTVTextFieldState> phoneKey =
      GlobalKey<CustomTVTextFieldState>();

  @override
  void initState() {
    super.initState();
    screenFocusNode.requestFocus();
  }

  @override
  void dispose() {
    screenFocusNode.dispose();
    emailController.dispose();
    passwordController.dispose();
    phoneController.dispose();
    _currentSection.dispose();
    _hasKeyboardOpen.dispose();
    super.dispose();
  }

  bool _canHandleKeys() => screenFocusNode.hasFocus && !_hasKeyboardOpen.value;

  KeyEventResult _handleKeyEvent(KeyEvent event) {
    if (!_canHandleKeys() ||
        (event is! KeyDownEvent && event is! KeyRepeatEvent)) {
      return KeyEventResult.ignored;
    }

    final handlers = {
      LogicalKeyboardKey.arrowUp: () => _navigate(-1),
      LogicalKeyboardKey.arrowDown: () => _navigate(1),
      LogicalKeyboardKey.enter: _handleSelect,
      LogicalKeyboardKey.select: _handleSelect,
    };

    final handler = handlers[event.logicalKey];
    if (handler != null) {
      handler();
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  }

  void _navigate(int delta) {
    final nextIndex =
        (LoginSection.values.indexOf(_currentSection.value) + delta);
    if (nextIndex >= 0 && nextIndex < LoginSection.values.length) {
      _currentSection.value = LoginSection.values[nextIndex];
    }
  }

  void _handleSelect() {
    switch (_currentSection.value) {
      case LoginSection.email:
        emailKey.currentState?.toggleKeyboard();
        break;
      case LoginSection.password:
        passwordKey.currentState?.toggleKeyboard();
        break;
      case LoginSection.phone:
        phoneKey.currentState?.toggleKeyboard();
        break;
      case LoginSection.loginButton:
        _submitLogin();
        break;
    }
  }

  void _submitLogin() {
    if (_formKey.currentState?.validate() ?? false) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Logging in as ${emailController.text}...'),
          backgroundColor: Colors.green,
          duration: const Duration(seconds: 2),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      focusNode: screenFocusNode,
      onKeyEvent: (_, event) => _handleKeyEvent(event),
      child: Scaffold(
        body: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 400),
            child: Form(
              key: _formKey,
              child: ValueListenableBuilder<LoginSection>(
                valueListenable: _currentSection,
                builder: (context, section, _) => ValueListenableBuilder<bool>(
                  valueListenable: _hasKeyboardOpen,
                  builder: (context, hasKeyboardOpen, _) => Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      const Text(
                        'TV Login',
                        style: TextStyle(
                          fontSize: 32,
                          fontWeight: FontWeight.bold,
                        ),
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 48),
                      _Field(
                        fieldKey: emailKey,
                        controller: emailController,
                        label: "Email",
                        icon: Icons.email,
                        isSelected:
                            section == LoginSection.email && !hasKeyboardOpen,
                        isRequired: true,
                        textFieldType: TextFieldType.EMAIL,
                        onVisibilityChanged: (v) => _hasKeyboardOpen.value = v,
                      ),
                      const SizedBox(height: 16),
                      _Field(
                        fieldKey: passwordKey,
                        controller: passwordController,
                        label: "Password",
                        icon: Icons.lock,
                        isSelected:
                            section == LoginSection.password &&
                            !hasKeyboardOpen,
                        isRequired: true,
                        textFieldType: TextFieldType.PASSWORD,
                        onVisibilityChanged: (v) => _hasKeyboardOpen.value = v,
                      ),
                      const SizedBox(height: 16),
                      _Field(
                        fieldKey: phoneKey,
                        controller: phoneController,
                        label: "Phone",
                        icon: Icons.phone,
                        isSelected:
                            section == LoginSection.phone && !hasKeyboardOpen,
                        isRequired: false,
                        textFieldType: TextFieldType.PHONE,
                        keyboardType: KeyboardType.numeric,
                        onVisibilityChanged: (v) => _hasKeyboardOpen.value = v,
                      ),
                      const SizedBox(height: 32),
                      _LoginButton(
                        isSelected:
                            section == LoginSection.loginButton &&
                            !hasKeyboardOpen,
                        onTap: _submitLogin,
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _Field extends StatelessWidget {
  final GlobalKey<CustomTVTextFieldState> fieldKey;
  final TextEditingController controller;
  final String label;
  final IconData icon;
  final bool isSelected;
  final ValueChanged<bool> onVisibilityChanged;
  final VoidCallback? onSubmitted;
  final KeyboardType keyboardType;
  final String? Function(String?)? validator;
  final bool isRequired;
  final TextFieldType textFieldType;

  const _Field({
    required this.fieldKey,
    required this.controller,
    required this.label,
    required this.icon,
    required this.isSelected,
    required this.onVisibilityChanged,
    this.onSubmitted,
    this.keyboardType = KeyboardType.alphabetic,
    this.validator,
    this.isRequired = false,
    this.textFieldType = TextFieldType.OTHER,
  });

  @override
  Widget build(BuildContext context) {
    return CustomTVTextField(
      key: fieldKey,
      controller: controller,
      hint: label,
      prefixIcon: Icon(icon, color: Colors.white70),
      isFocused: isSelected,
      onVisibilityChanged: onVisibilityChanged,
      onFieldSubmitted: onSubmitted != null ? (_) => onSubmitted!() : null,
      keyboardType: keyboardType,
      backgroundColor: Colors.grey[900],
      focusedBorderColor: Colors.white,
      borderRadius: 4,
      validator: validator,
      isRequired: isRequired,
      textFieldType: textFieldType,
    );
  }
}

class _LoginButton extends StatelessWidget {
  final bool isSelected;
  final VoidCallback onTap;

  const _LoginButton({required this.isSelected, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      height: 52,
      decoration: BoxDecoration(
        color: isSelected
            ? Colors.deepPurpleAccent
            : Colors.deepPurple.withValues(alpha: 0.3),
        borderRadius: BorderRadius.circular(4),
        border: Border.all(
          color: isSelected ? Colors.white : Colors.transparent,
          width: 2,
        ),
      ),
      child: InkWell(
        onTap: onTap,
        child: const Center(
          child: Text(
            "SIGN IN",
            style: TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
              letterSpacing: 1.2,
            ),
          ),
        ),
      ),
    );
  }
}
7
likes
0
points
190
downloads

Publisher

unverified uploader

Weekly Downloads

A premium, TV-optimized custom text field and keyboard for Flutter applications. Support for remote control navigation, automated validation, and smooth animations.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on custom_tv_text_field