coocaa_flutter_focus 0.4.4 copy "coocaa_flutter_focus: ^0.4.4" to clipboard
coocaa_flutter_focus: ^0.4.4 copied to clipboard

A Flutter focus and interaction library for TV remotes, directional keyboards, gamepads, and touch input.

coocaa_flutter_focus #

Version Flutter Dart Input Tests License: MIT

coocaa_flutter_focus banner

coocaa_flutter_focus 0.4.4 is a Flutter focus and interaction library for TV apps, remote controls, directional keyboards, gamepads, and touch input.

It provides a small public API around Flutter's FocusNode system, with behavior tuned for large-screen interfaces: arrow-key traversal, focus groups, focus memory, edge handling, focus-id lookup, and automatic scroll visibility.

Platform Support #

This package uses Flutter framework APIs only. It has no native plugin code and no platform-specific imports.

It is best suited for Android TV, large-screen Android apps, desktop keyboard apps, and web keyboard navigation. iOS is supported at the framework level when an external keyboard or directional input is available. Pointer actions work on every Flutter platform that provides touch, mouse, or stylus input.

Features #

  • Global directional focus coordination through FocusController.
  • Focus registration and lifecycle handling through FocusableWidget.
  • Logical focus areas through FocusableGroup.
  • Group edge modes: crossing, greedy, and blocked.
  • Group inner-focus modes: greedy and crossing.
  • Group focus memory and onBeforeFocusEnter overrides.
  • Focus lookup and request by string focusId.
  • Automatic scrolling for single taps and long-press directional movement.
  • Configurable scroll edge offset and momentum through FocusScrollConfig.
  • Scroll lifecycle observation through addScrollListener and FocusScrollEvent.
  • Back-key interception with newest-first callback order.
  • Unified tap, double-tap, and long-press actions for touch, mouse, keyboard, TV remote, and gamepad input.
  • Runtime switches for pointer support and all focus-system interaction.
  • Test coverage for traversal, nested groups, scrolling, long press, and cache invalidation.

Installation #

Add the package to your Flutter project:

dependencies:
  coocaa_flutter_focus: ^0.4.4

For local development:

dependencies:
  coocaa_flutter_focus:
    path: ../coocaa_flutter_focus

Then import the public library entry:

import 'package:coocaa_flutter_focus/coocaa_flutter_focus.dart';

Quick Start #

Initialize the controller once near app startup. Use FocusController.instance.navigatorKey if you want the controller to coordinate with your app navigator.

Key setup

FocusController.instance
  ..init()
  ..updateConfig(scrollEdgeOffset: 80);

MaterialApp(
  navigatorKey: FocusController.instance.navigatorKey,
  home: const FocusDemoPage(),
);
import 'package:coocaa_flutter_focus/coocaa_flutter_focus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FocusController.instance
    ..init()
    ..updateConfig(scrollEdgeOffset: 80);

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: FocusController.instance.navigatorKey,
      home: const FocusDemoPage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return FocusableGroup(
      edgeFocusMode: FocusableGroupEdgeFocusMode.crossing,
      child: Row(
        children: List<Widget>.generate(4, (int index) {
          return Padding(
            padding: const EdgeInsets.all(8),
            child: FocusableWidget(
              focusId: 'card-$index',
              autofocus: index == 0,
              onEdge: (FocusNode node, LogicalKeyboardKey direction) {
                debugPrint('Reached edge: $direction');
                return null;
              },
              child: Builder(
                builder: (BuildContext context) {
                  final bool focused = Focus.of(context).hasFocus;
                  return AnimatedContainer(
                    duration: const Duration(milliseconds: 120),
                    width: 160,
                    height: 96,
                    alignment: Alignment.center,
                    decoration: BoxDecoration(
                      color: focused ? Colors.blue : Colors.grey.shade700,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Text(
                      'Card $index',
                      style: const TextStyle(color: Colors.white),
                    ),
                  );
                },
              ),
            ),
          );
        }),
      ),
    );
  }
}

Dispose the controller when the owning app or test surface is torn down:

@override
void dispose() {
  FocusController.instance.dispose();
  super.dispose();
}

FocusController #

FocusController.instance is the global coordinator.

Common methods:

  • init() registers keyboard, focus, and metrics listeners.
  • dispose() removes listeners, stops pending scroll activity, and clears controller state.
  • updateConfig(...) updates scrolling and input behavior at runtime. Use touchEnabled to control pointer actions and interactionEnabled to suspend directional navigation and all FocusableWidget actions.
  • setScrollConfig(config) updates only FocusScrollConfig.
  • clearScrollEdgeOffset() removes the configured edge offset.
  • isDirectionKey(key) returns true for arrow keys.
  • findNextFocusNode(direction) resolves the next candidate without requesting focus.
  • requestFocus(node, direction: ..., scrollable: true) requests focus and optionally scrolls the node into view.
  • findFocusableById(id) returns a registered node by focusId.
  • requestFocusById(id, direction: ..., scrollable: true) requests focus by focusId.
  • addBackInterceptor(callback) intercepts physical and system back requests. It returns a remover callback.
  • removeBackInterceptor(callback) removes a previously registered back interceptor.
  • handleBack() runs registered back interceptors before navigation. Use it when application code explicitly owns the back action.
  • animateScrollPositionTo(position, target, ...) runs the same scroll animator used by focus movement for a specific ScrollPosition.
  • stopScrollPosition(position) stops an active focus scroll animation for a specific ScrollPosition.
  • addScrollListener(callback) observes controller-driven scroll start, update, end, and cancel events. It returns a remover callback.

Physical back keys run when the matching key is released. Physical and system back requests run BackInterceptor callbacks newest first before popup dismissal or route navigation; return true to consume the request. When no interceptor consumes it, the back request closes a focused popup first, then calls Navigator.maybePop(), and exits only when no route handles the request.

Low-level registration methods:

  • registerFocusable(...) and unregisterFocusable(node) are used by FocusableWidget. Call them directly only when building a custom focusable wrapper.
  • registerGroup(...) and unregisterGroup(groupKey) are used by FocusableGroup. Call them directly only when building a custom group wrapper.

FocusableWidget #

Wrap every focusable item with FocusableWidget.

Important properties:

  • focusNode: provide your own node, or let the widget create one.
  • autofocus: request initial focus through Flutter's focus system.
  • canRequestFocus and skipTraversal: mirror standard Focus behavior.
  • autoScroll: allow or disable focus-driven scroll alignment for this item.
  • focusId: register the node for findFocusableById and requestFocusById.
  • debugLabel: label the internally created node.
  • onKeyEvent: handle custom key events before default traversal.
  • onFocusChange: observe focus changes.
  • onDirection: override directional traversal. Return a target node to take over the move, return the same node to consume the repeat, or return null to use default traversal.
  • onEdge: observe or override edge behavior.
  • onInitNode: receive the effective FocusNode.
  • onTap, onDoubleTap, and onLongPress: receive the same FocusableActionDetails shape for pointer and keyboard-style activation.
  • onTapStateChanged: observe pressed state transitions while an activation is pending or being recognized.
  • enableLongPress: enable or disable long-press recognition independently of the onLongPress callback.
  • requestFocusOnPointerAction: request focus before a pointer action callback; defaults to true.
  • activationKeys: customize the keyboard, remote, and gamepad keys that activate this widget.

Unified Actions #

FocusableWidget exposes the same action callbacks for touch, mouse, keyboard, TV remote, and gamepad input:

FocusableWidget(
  onTap: (FocusableActionDetails details) {
    openItem();
  },
  onDoubleTap: (FocusableActionDetails details) {
    addToFavorites();
  },
  onLongPress: (FocusableActionDetails details) {
    openContextMenu();
  },
  child: const ItemCard(),
)

Use details.source to distinguish FocusableActionSource.pointer from FocusableActionSource.keyboard. Pointer actions include positions and the pointer kind; keyboard-style actions include the triggering logical key.

The default activation keys are Select, Enter, Numpad Enter, Space, and gamepad A. A double tap suppresses its single-tap callbacks, and a recognized long press suppresses the tap on release. When onDoubleTap is configured, a single tap waits for Flutter's double-tap window before firing.

Input Configuration #

Both global switches default to true and can be changed at runtime:

FocusController.instance.updateConfig(
  touchEnabled: true,
  interactionEnabled: true,
);
  • touchEnabled: false removes FocusableWidget pointer gesture recognizers. Keyboard and TV remote actions continue to work.
  • interactionEnabled: false consumes direction keys before traversal and disables tap, double-tap, and long-press actions from every input source. Back-key interception and custom raw onKeyEvent handling remain available.
  • Re-enabling either switch updates existing actionable widgets immediately. Disabling interaction also cancels pending action timers and directional long-press state.

FocusableGroup #

Use FocusableGroup to model a row, panel, section, list, dialog, or any logical focus area.

Important properties:

  • limitDirections: directions that may be constrained at this group's edge.
  • edgeFocusMode: controls how traversal behaves when the group has no in-group target.
  • innerFocusMode: controls whether in-group search accepts all directional candidates or only cross-axis-overlapping candidates. It defaults to FocusableGroupInnerFocusMode.greedy.
  • memory: restore the last focused child when entering the group.
  • onGroupFocusChange: reports group enter and leave state.
  • onBeforeFocusEnter: choose a child before group memory or geometry wins.
  • onEdge: observe or override group edge behavior.
  • edgePadding: reserve extra viewport space for this group when scrolling.
  • scrollCenter: center group targets when focus-driven scroll alignment runs.

Geometry #

FocusableGroup uses the size of its child and does not expand simply because it wraps a widget. Provide explicit constraints in application layout, such as SizedBox, Expanded, or Positioned, when the group should represent a larger panel or viewport. Directional entry compares the group boundary itself; focus memory chooses the child to enter but does not alter that boundary.

Edge Modes #

  • FocusableGroupEdgeFocusMode.crossing: the default. Leaving the group prefers candidates that geometrically cross the current edge.
  • FocusableGroupEdgeFocusMode.greedy: if no in-group candidate is found, continue to the nearest candidate on the requested side.
  • FocusableGroupEdgeFocusMode.blocked: block configured limitDirections; unconfigured directions behave like greedy traversal.

Inner Focus Modes #

  • FocusableGroupInnerFocusMode.greedy: the default. Search any valid directional target inside the current group before searching outside it.
  • FocusableGroupInnerFocusMode.crossing: only accept in-group targets that overlap the current focus on the cross axis. If none qualifies, the existing edge mode and direction limits decide whether to search outside the group.

Scroll Tuning #

Use FocusScrollConfig for single-tap and long-press scroll motion:

FocusController.instance.updateConfig(
  scrollEdgeOffset: 96,
  scrollConfig: const FocusScrollConfig(
    singleTapDuration: Duration(milliseconds: 380),
    singleTapMinDuration: Duration(milliseconds: 200),
    singleTapVelocity: 1250,
    singleTapRetargetVelocity: 3000,
    longPressStartDelay: Duration(milliseconds: 140),
    longPressAccelerationDuration: Duration(milliseconds: 520),
    longPressInitialVelocity: 520,
    longPressMaxVelocity: 3600,
  ),
);

scrollEdgeOffset keeps focused content away from the viewport edge. Group edgePadding can further constrain focus scroll behavior inside nested or large content sections.

Testing #

Widget tests should initialize and dispose the controller explicitly:

void main() {
  setUp(() {
    FocusController.instance.init();
  });

  tearDown(() {
    FocusController.instance.dispose();
  });
}

Run static analysis:

flutter analyze

Run tests:

flutter test

Exported API #

Import only the public library entry:

import 'package:coocaa_flutter_focus/coocaa_flutter_focus.dart';

The package exports:

  • FocusController
  • FocusScrollConfig
  • FocusableWidget
  • FocusableActionCallback
  • FocusableActionDetails
  • FocusableActionSource
  • FocusableActionType
  • defaultFocusableActivationKeys
  • FocusableGroup
  • FocusableGroupEdgeFocusMode
  • FocusableGroupInnerFocusMode
  • FocusDirectionCallback
  • FocusEdgeCallback
  • GroupFocusChangeCallback
  • GroupBeforeFocusEnterCallback
  • BackInterceptor
  • FocusScrollEvent
  • FocusScrollListener
  • FocusScrollPhase
  • FocusScrollSource

Contact #

For questions or feedback, contact wuronghua@coocaa.com.

1
likes
140
points
68
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter focus and interaction library for TV remotes, directional keyboards, gamepads, and touch input.

Repository
View/report issues

Topics

#flutter #focus #tv #keyboard #remote-control

License

MIT (license)

Dependencies

flutter

More

Packages that depend on coocaa_flutter_focus