Skip to content

fluttercandies/dpad

Repository files navigation

📺 Dpad
Flutter TV Navigation System

中文文档

Dpad Logo


Pub Version Platform License

A simple yet powerful D-pad navigation system that makes Flutter development for Android TV, Fire TV, and other TV platforms as easy as native Android development.

✨ Features

  • 🎯 Simple Setup: Just 3 steps to get started
  • 🎨 Customizable Effects: Built-in focus effects + custom builders
  • 📺 Platform Support: Android TV, Fire TV, Apple TV, and more
  • Performance: Optimized for smooth navigation
  • 🔧 Programmatic Control: Full API for programmatic navigation
  • 🎮 Game Controller Support: Works with standard controllers
  • 🔄 Sequential Navigation: Previous/Next support for media and lists

🚀 Quick Start

1. Add Dependency

dependencies:
  dpad: any

2. Wrap Your App

import 'package:dpad/dpad.dart';

void main() {
  runApp(
    DpadNavigator(
      enabled: true,
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

3. Make Widgets Focusable

class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        DpadFocusable(
          autofocus: true,
          onFocus: () => print('Focused'),
          onSelect: () => print('Selected'),
          builder: (context, isFocused, child) {
            return AnimatedContainer(
              duration: Duration(milliseconds: 200),
              decoration: BoxDecoration(
                border: Border.all(
                  color: isFocused ? Colors.blue : Colors.transparent,
                  width: 3,
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: child,
            );
          },
          child: ElevatedButton(
            onPressed: () => print('Pressed'),
            child: Text('Button 1'),
          ),
        ),
        
        DpadFocusable(
          onSelect: () => print('Button 2 selected'),
          child: ElevatedButton(
            onPressed: () => print('Pressed'),
            child: Text('Button 2'),
          ),
        ),
      ],
    );
  }
}

🎨 Focus Effects

Built-in Effects

// Border highlight
DpadFocusable(
  builder: FocusEffects.border(color: Colors.blue),
  child: MyWidget(),
)

// Glow effect
DpadFocusable(
  builder: FocusEffects.glow(glowColor: Colors.blue),
  child: MyWidget(),
)

// Scale effect
DpadFocusable(
  builder: FocusEffects.scale(scale: 1.1),
  child: MyWidget(),
)

// Gradient background
DpadFocusable(
  builder: FocusEffects.gradient(
    focusedColors: [Colors.blue, Colors.purple],
  ),
  child: MyWidget(),
)

// Combine multiple effects
DpadFocusable(
  builder: FocusEffects.combine([
    FocusEffects.scale(scale: 1.05),
    FocusEffects.border(color: Colors.blue),
  ]),
  child: MyWidget(),
)

Custom Effects

DpadFocusable(
  builder: (context, isFocused, child) {
    return Transform.scale(
      scale: isFocused ? 1.1 : 1.0,
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          boxShadow: isFocused ? [
            BoxShadow(
              color: Colors.blue.withValues(alpha: 0.6), // ignore: deprecated_member_use
              blurRadius: 20,
              spreadRadius: 2,
            ),
          ] : null,
        ),
        child: child,
      ),
    );
  },
  child: Container(
    child: Text('Custom Effect'),
  ),
)

🔧 Advanced Usage

📜 Auto-Scroll (New in v1.2.2)

DpadFocusable now automatically scrolls to ensure the focused widget is fully visible, including focus effects like glow and borders.

DpadFocusable(
  autoScroll: true,           // Enable auto-scroll (default: true)
  scrollPadding: 24.0,        // Extra padding for focus effects (default: 24.0)
  builder: FocusEffects.glow(glowColor: Colors.blue),
  child: MyWidget(),
)

// Disable auto-scroll for specific widgets
DpadFocusable(
  autoScroll: false,
  child: MyWidget(),
)

// Programmatic scroll control
Dpad.scrollToFocus(
  focusNode,
  padding: 32.0,
  duration: Duration(milliseconds: 300),
  curve: Curves.easeOutCubic,
);

🧠 Focus Memory (Updated in v1.2.2)

The focus memory system intelligently remembers user's focus positions and restores them when navigating back, providing a more natural TV navigation experience.

Quick Setup

DpadNavigator(
  focusMemory: FocusMemoryOptions(
    enabled: true,
    maxHistory: 20,
  ),
  onNavigateBack: (context, previousEntry, history) {
    if (previousEntry != null) {
      previousEntry.focusNode.requestFocus();
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  },
  child: MyApp(),
)

Region Identification

// Tab bar
DpadFocusable(
  region: 'tabs',
  child: TabButton(),
)

// Filter area
DpadFocusable(
  region: 'filters',
  child: FilterOption(),
)

// Content cards
DpadFocusable(
  region: 'cards',
  child: ContentCard(),
)

Use Cases

  • Tab Navigation: Tab A → Browse → Tab B → Back → Tab B (restores previous tab)
  • Filter Navigation: Filter A → Browse → Filter A → Back → Filter A (restores previous filter)
  • Cross-Route Navigation: Maintains separate focus history per route

🎯 Region-based Navigation (New in v2.0.0)

Region-based navigation solves the common TV UX problem where Flutter's default geometric navigation doesn't match user expectations.

The Problem

With default Flutter navigation:

  • Tab → Content: might focus any card based on distance
  • Content → Tab: might jump to unexpected tab
  • Sidebar → Grid: focus could land anywhere

The Solution

DpadNavigator(
  regionNavigation: RegionNavigationOptions(
    enabled: true,
    rules: [
      // Tab → Content: always focus first card
      RegionNavigationRule(
        fromRegion: 'tabs',
        toRegion: 'content',
        direction: TraversalDirection.down,
        strategy: RegionNavigationStrategy.fixedEntry,
        bidirectional: true,
        reverseStrategy: RegionNavigationStrategy.memory,
      ),
      // Sidebar → Grid: always focus first card
      RegionNavigationRule(
        fromRegion: 'sidebar',
        toRegion: 'grid',
        direction: TraversalDirection.right,
        strategy: RegionNavigationStrategy.fixedEntry,
      ),
    ],
  ),
  child: MyApp(),
)

Mark Entry Points

// First card in content area - entry point
DpadFocusable(
  region: 'content',
  isEntryPoint: true,
  child: ContentCard(),
)

// Other cards in the same region
DpadFocusable(
  region: 'content',
  child: ContentCard(),
)

Navigation Strategies

Strategy Behavior
geometric Flutter's default distance-based navigation
fixedEntry Always focus the widget marked as isEntryPoint
memory Restore last focused widget, fallback to entry point
custom Use custom resolver function

Custom Shortcuts

DpadNavigator(
  customShortcuts: {
    LogicalKeyboardKey.keyG: () => _showGridView(),
    LogicalKeyboardKey.keyL: () => _showListView(),
    LogicalKeyboardKey.keyR: () => _refreshData(),
    LogicalKeyboardKey.keyS: () => _showSearch(),
  },
  onMenuPressed: () => _showMenu(),
  onBackPressed: () => _handleBack(),
  child: MyApp(),
)

Default Keyboard Shortcuts (v1.1.0+):

  • Arrow Keys: Directional navigation (up, down, left, right)
  • Tab/Shift+Tab: Sequential navigation (next/previous)
  • Media Track Next/Previous: Media control navigation
  • Channel Up/Down: TV remote sequential navigation
  • Enter/Select/Space: Trigger selection action
  • Escape/Back: Navigate back
  • ContextMenu: Show menu

Programmatic Navigation

// Navigate in directions
Dpad.navigateUp(context);
Dpad.navigateDown(context);
Dpad.navigateLeft(context);
Dpad.navigateRight(context);

// Sequential navigation (new in v1.1.0)
Dpad.navigateNext(context);      // Tab / Media Track Next
Dpad.navigatePrevious(context);   // Shift+Tab / Media Track Previous

// Focus management
final currentFocus = Dpad.currentFocus;
Dpad.requestFocus(myFocusNode);
Dpad.clearFocus();

Platform-Specific Handling

DpadNavigator(
  onMenuPressed: () {
    // Handle menu button on TV remotes
    _showMenu();
  },
  onBackPressed: () {
    // Handle back button
    if (Navigator.canPop(context)) {
      Navigator.pop(context);
    }
  },
  child: MyApp(),
)

📱 Platform Support

  • Android TV: Full native D-pad support
  • Amazon Fire TV: Compatible with Fire TV remotes
  • Apple TV: Works with Siri Remote (Flutter web)
  • Game Controllers: Standard controller navigation
  • Generic TV Platforms: Any D-pad compatible input

💡 Best Practices

  1. Always set autofocus: true on one widget per screen for initial focus
  2. Test with real D-pad hardware, not just keyboard arrows
  3. Consider focus order - arrange widgets logically for navigation
  4. Provide clear visual feedback - use prominent focus indicators
  5. Handle edge cases - what happens when navigation fails?

🏗️ Architecture

The system consists of three main components:

  • DpadNavigator: Root widget that captures D-pad events
  • DpadFocusable: Wrapper that makes widgets focusable
  • Dpad: Utility class for programmatic control

All components work together seamlessly with Flutter's focus system.

🔄 Migration

Coming from other TV navigation libraries?

  • ✅ No complex configuration needed
  • ✅ Works with standard Flutter widgets
  • ✅ No custom FocusNode management required
  • ✅ Built-in support for all TV platforms
  • ✅ Extensive customization options

📖 Example

Check out the example app for a complete implementation showing:

  • Grid navigation
  • List navigation
  • Custom focus effects
  • Programmatic navigation
  • Platform-specific handling

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Flutter TV D-pad navigation system - as simple as native Android development

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published