flutter_hand_gesture 2.1.1
flutter_hand_gesture: ^2.1.1 copied to clipboard
A Flutter package for real-time hand gesture recognition using on-device ML. Supports custom gestures, landmark detection, and stream-based API. No internet required.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hand_gesture/flutter_hand_gesture.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hand Gesture Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: true),
home: const HomeShell(),
);
}
}
/// Hosts the three v2 demos behind a [BottomNavigationBar].
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _index = 0;
static const _tabs = [
TamilSignScreen(),
SequenceDemoScreen(),
PresentationScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(index: _index, children: _tabs),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.sign_language), label: 'Tamil'),
NavigationDestination(icon: Icon(Icons.timeline), label: 'Sequence'),
NavigationDestination(icon: Icon(Icons.slideshow), label: 'Present'),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Tab 1 — Tamil signs
// ─────────────────────────────────────────────────────────────────────────────
/// Recognizes Tamil/ISL signs from live landmarks using a [SignRecognizer],
/// which combines finger shape, hand orientation, and motion — so thumbs-up
/// (*nalla*) vs thumbs-down (*ketta*) and the wagging index (*illai*) are told
/// apart.
class TamilSignScreen extends StatefulWidget {
const TamilSignScreen({super.key});
@override
State<TamilSignScreen> createState() => _TamilSignScreenState();
}
class _TamilSignScreenState extends State<TamilSignScreen> {
final _recognizer = SignRecognizer<TamilSign>(TamilSignPreset.signs);
TamilSign? _sign;
void _onResult(GestureResult result) {
final hand = result.primaryHand;
if (hand == null) {
_recognizer.reset();
return;
}
final sign = _recognizer.update(hand.landmarks, result.timestamp);
if (sign?.name != _sign?.name) {
setState(() => _sign = sign);
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
GestureCameraView(
// Zero debounce so every frame reaches the recognizer — motion
// (the wagging index of *illai*) needs a continuous stream.
config: const GestureConfig(
maxHands: 1,
showLandmarks: true,
debounceDuration: Duration.zero,
),
showGestureLabel: false,
onResult: _onResult,
),
const Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'தமிழ் சைகை — Tamil Signs',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
),
if (_sign != null)
Positioned(
bottom: 32,
left: 16,
right: 16,
child: _SignCard(sign: _sign!),
),
],
);
}
}
class _SignCard extends StatelessWidget {
final TamilSign sign;
const _SignCard({required this.sign});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.cyanAccent.withValues(alpha: 0.5)),
),
child: Row(
children: [
Text(sign.emoji, style: const TextStyle(fontSize: 44)),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sign.tamilScript,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
Text(
sign.name,
style: const TextStyle(
color: Colors.cyanAccent,
fontSize: 16,
),
),
Text(
sign.meaning,
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
],
),
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Tab 2 — Sequence demo
// ─────────────────────────────────────────────────────────────────────────────
/// Registers a couple of gesture sequences and shows live progress dots, firing
/// a banner when one completes.
class SequenceDemoScreen extends StatefulWidget {
const SequenceDemoScreen({super.key});
@override
State<SequenceDemoScreen> createState() => _SequenceDemoScreenState();
}
class _SequenceDemoScreenState extends State<SequenceDemoScreen> {
final _recognizer = GestureSequenceRecognizer()
..registerAll([
const GestureSequence(
name: 'unlock',
label: 'Unlock',
steps: [
GestureType.thumbsUp,
GestureType.peace,
GestureType.openPalm,
],
),
const GestureSequence(
name: 'dismiss',
label: 'Dismiss',
steps: [GestureType.openPalm, GestureType.fist],
),
]);
Map<String, int> _progress = {};
String? _fired;
void _onGesture(HandGesture gesture) {
final matched = _recognizer.update(gesture.type, gesture.timestamp);
setState(() {
_progress = _recognizer.progress;
if (matched != null) _fired = matched.label;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
GestureCameraView(
config: const GestureConfig(maxHands: 1),
onGestureDetected: _onGesture,
),
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sequences',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'unlock: 👍 → ✌️ → 🖐 · dismiss: 🖐 → ✊',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 12,
),
),
const SizedBox(height: 16),
_ProgressRows(
recognizer: _recognizer,
progress: _progress,
),
],
),
),
),
),
if (_fired != null)
Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.greenAccent.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(24),
),
child: Text(
'✅ ${_fired!}',
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
}
/// Renders a labeled progress-dot row per sequence using
/// [SequenceProgressPainter].
class _ProgressRows extends StatelessWidget {
final GestureSequenceRecognizer recognizer;
final Map<String, int> progress;
const _ProgressRows({required this.recognizer, required this.progress});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final seq in recognizer.sequences)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
SizedBox(
width: 90,
child: Text(
seq.label,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
CustomPaint(
size: Size(seq.steps.length * 20.0, 16),
painter: SequenceProgressPainter(
progress: {seq.name: progress[seq.name] ?? 0},
totalSteps: {seq.name: seq.steps.length},
),
),
],
),
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Tab 3 — Presentation mode
// ─────────────────────────────────────────────────────────────────────────────
/// Maps gestures to slide actions via [GestureActionMapper.presentation].
class PresentationScreen extends StatefulWidget {
const PresentationScreen({super.key});
@override
State<PresentationScreen> createState() => _PresentationScreenState();
}
class _PresentationScreenState extends State<PresentationScreen> {
final _mapper = GestureActionMapper.presentation();
int _slide = 1;
static const _totalSlides = 10;
String? _lastAction;
bool _blackScreen = false;
void _onGesture(HandGesture gesture) {
final action = _mapper.resolve(gesture.type);
if (action == null) return;
setState(() {
_lastAction = action;
switch (action) {
case 'next_slide':
_slide = (_slide + 1).clamp(1, _totalSlides);
_blackScreen = false;
case 'prev_slide':
_slide = (_slide - 1).clamp(1, _totalSlides);
_blackScreen = false;
case 'black_screen':
_blackScreen = !_blackScreen;
case 'pause_timer':
break;
}
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Slide canvas
Positioned.fill(
child: ColoredBox(
color: _blackScreen ? Colors.black : const Color(0xFF1A237E),
child: Center(
child: _blackScreen
? const Text(
'Black screen',
style: TextStyle(color: Colors.white24, fontSize: 18),
)
: Text(
'Slide $_slide / $_totalSlides',
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Small camera preview corner
Positioned(
bottom: 24,
right: 16,
width: 130,
height: 180,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: GestureCameraView(
config: const GestureConfig(maxHands: 1),
showGestureLabel: false,
showLandmarks: false,
onGestureDetected: _onGesture,
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Presentation Mode',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'👆 next · 👎 prev · ✊ black · 🖐 pause',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 12,
),
),
if (_lastAction != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'action: $_lastAction',
style: const TextStyle(
color: Colors.cyanAccent,
fontSize: 13,
),
),
),
],
),
),
),
),
],
);
}
}