m3e_haptics 0.0.1 copy "m3e_haptics: ^0.0.1" to clipboard
m3e_haptics: ^0.0.1 copied to clipboard

PlatformAndroid

High-fidelity AOSP-aligned native haptic feedback compositions for Flutter.

example/lib/main.dart

// Copyright (c) 2026 Mudit Purohit
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

import 'package:flutter/material.dart';
import 'package:m3e_haptics/m3e_haptics.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'M3E Haptics Demo',
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.dark,
      darkTheme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.dark,
          surface: const Color(0xFF141218),
        ),
      ),
      home: const HapticsDemoScreen(),
    );
  }
}

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

  @override
  State<HapticsDemoScreen> createState() => _HapticsDemoScreenState();
}

class _HapticsDemoScreenState extends State<HapticsDemoScreen> {
  // Slider values
  double _sliderValue = 0.5;
  M3EHapticTracker? _sliderTracker;

  // Slider Config Controls
  bool _enableContinuousDrag = true;
  bool _vibrateOnBookends = true;
  double _dragThreshold = 0.015;

  // Loader state
  double _loaderProgress = 0.0;
  bool _isLoaderRunning = false;
  M3EHapticTracker? _loaderTracker;

  // Swipe items state
  final List<String> _swipeItems =
      List.generate(4, (i) => 'Tactile Card Item #${i + 1}');
  final Map<String, double> _swipeProgress = {};
  final Map<String, M3EHapticTracker> _swipeTrackers = {};

  @override
  void initState() {
    super.initState();
    _initSliderTracker();
  }

  void _initSliderTracker() {
    _sliderTracker = M3EHapticTracker(
      baseHaptic: M3EHapticFeedback.light,
      config: M3EHapticConfig(
        enableContinuousDrag: _enableContinuousDrag,
        vibrateOnLowerBookend: _vibrateOnBookends,
        vibrateOnUpperBookend: _vibrateOnBookends,
        deltaProgressForDragThreshold: _dragThreshold,
        lowerBookendThreshold: 0.01,
        upperBookendThreshold: 0.99,
      ),
    );
  }

  M3EHapticTracker _createLoaderTracker() {
    return M3EHapticTracker(
      baseHaptic: M3EHapticFeedback.light,
      config: const M3EHapticConfig(
        enableContinuousDrag: true,
        deltaProgressForDragThreshold: 0.05,
        vibrateOnLowerBookend: true,
        vibrateOnUpperBookend: true,
        lowerBookendThreshold: 0.01,
        upperBookendThreshold: 0.99,
      ),
    );
  }

  M3EHapticTracker _createSwipeTracker() {
    return M3EHapticTracker(
      baseHaptic: M3EHapticFeedback.light,
      config: const M3EHapticConfig(
        enableContinuousDrag: true,
        deltaProgressForDragThreshold: 0.04,
        vibrateOnLowerBookend: false,
        vibrateOnUpperBookend: false,
        progressBasedDragMinScale: 0.10,
        progressBasedDragMaxScale: 0.85,
      ),
    );
  }

  // Simulate loader progress
  void _runLoader() async {
    if (_isLoaderRunning) return;
    setState(() {
      _isLoaderRunning = true;
      _loaderProgress = 0.0;
    });

    _loaderTracker = _createLoaderTracker();

    while (_loaderProgress < 1.0 && _isLoaderRunning) {
      await Future.delayed(const Duration(milliseconds: 100));
      if (!mounted || !_isLoaderRunning) break;
      setState(() {
        _loaderProgress = (_loaderProgress + 0.05).clamp(0.0, 1.0);
      });
    }

    setState(() {
      _isLoaderRunning = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('M3E Haptics Showcase'),
        centerTitle: true,
        backgroundColor: cs.surfaceContainerHighest.withValues(alpha: 0.3),
        elevation: 0,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // 1. Slider Haptic Integration Demo
            _buildSliderDemoSection(cs),
            const SizedBox(height: 16),

            // 2. Determinate Loader Demo
            _buildLoaderDemoSection(cs),
            const SizedBox(height: 16),

            // 3. Swipe-to-Dismiss Resistance Demo
            _buildSwipeDemoSection(cs),
            const SizedBox(height: 16),

            // 4. Raw Composition Presets
            _buildPresetsSection(cs),
          ],
        ),
      ),
    );
  }

  Widget _buildSliderDemoSection(ColorScheme cs) {
    return _buildCardWrapper(
      title: 'Interactive AOSP Slider',
      subtitle:
          'Continuous drag textures, dynamic velocity bumps, and bookends.',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Volume / Progress Level: ${(_sliderValue * 100).round()}%',
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const Text('Swipe the slider handle back and forth',
                        style: TextStyle(fontSize: 12, color: Colors.grey)),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          M3EHapticListener(
            tracker: _sliderTracker!,
            progress: _sliderValue,
            child: Slider(
              value: _sliderValue,
              onChanged: (val) {
                setState(() {
                  _sliderValue = val;
                });
              },
            ),
          ),
          const SizedBox(height: 16),
          const Divider(),
          // Config controls
          SwitchListTile(
            title: const Text('Continuous Drag Haptics'),
            subtitle: const Text('Subtle LOW_TICK feedback on movement'),
            value: _enableContinuousDrag,
            onChanged: (val) {
              setState(() => _enableContinuousDrag = val);
              _initSliderTracker();
            },
            contentPadding: EdgeInsets.zero,
          ),
          SwitchListTile(
            title: const Text('Lower/Upper Bookend haptics'),
            subtitle: const Text('Firm CLICK boundary hit feels'),
            value: _vibrateOnBookends,
            onChanged: (val) {
              setState(() => _vibrateOnBookends = val);
              _initSliderTracker();
            },
            contentPadding: EdgeInsets.zero,
          ),
          ListTile(
            title: const Text('Drag Tick Interval'),
            subtitle: Text(
                'Distance threshold: ${_dragThreshold.toStringAsFixed(3)}'),
            contentPadding: EdgeInsets.zero,
            trailing: SizedBox(
              width: 140,
              child: Slider(
                value: _dragThreshold,
                min: 0.005,
                max: 0.05,
                divisions: 9,
                onChanged: _enableContinuousDrag
                    ? (val) {
                        setState(() => _dragThreshold = val);
                        _initSliderTracker();
                      }
                    : null,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLoaderDemoSection(ColorScheme cs) {
    return _buildCardWrapper(
      title: 'Determinate Loading Stream',
      subtitle: 'Vibrates incrementally matching loading states.',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Row(
            children: [
              M3EHapticListener(
                tracker: _loaderTracker ?? _createLoaderTracker(),
                progress: _loaderProgress,
                listenToPointer: false,
                child: CircularProgressIndicator(
                  value: _loaderProgress,
                  strokeWidth: 6,
                  backgroundColor: cs.surfaceContainerHighest,
                ),
              ),
              const SizedBox(width: 24),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                        'Loading progress: ${(_loaderProgress * 100).round()}%',
                        style: const TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    ElevatedButton(
                      onPressed: _isLoaderRunning ? null : _runLoader,
                      child: Text(_isLoaderRunning
                          ? 'Loading...'
                          : 'Start Loading Progress'),
                    ),
                  ],
                ),
              )
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildSwipeDemoSection(ColorScheme cs) {
    return _buildCardWrapper(
      title: 'Swipe Resistance Haptics',
      subtitle:
          'Continuous drag texture increases amplitude as card is swiped.',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Swipe card items horizontally. You will feel increasing tick vibration intensity as you pull further away from the center.',
            style: TextStyle(fontSize: 13, color: Colors.grey),
          ),
          const SizedBox(height: 12),
          ListView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            itemCount: _swipeItems.length,
            itemBuilder: (context, index) {
              final item = _swipeItems[index];
              final tracker = _swipeTrackers.putIfAbsent(
                item,
                _createSwipeTracker,
              );
              return Padding(
                padding: const EdgeInsets.only(bottom: 8.0),
                child: M3EHapticListener(
                  tracker: tracker,
                  progress: _swipeProgress[item] ?? 0.0,
                  child: Dismissible(
                    key: ValueKey(item),
                    onUpdate: (details) {
                      setState(() {
                        _swipeProgress[item] = details.progress;
                      });
                    },
                    onDismissed: (_) {
                      setState(() {
                        _swipeItems.removeAt(index);
                        _swipeProgress.remove(item);
                        _swipeTrackers.remove(item);
                      });
                    },
                    background: Container(
                      color: Colors.redAccent,
                      alignment: Alignment.centerLeft,
                      padding: const EdgeInsets.only(left: 20),
                      child: const Icon(Icons.delete, color: Colors.white),
                    ),
                    child: Card(
                      margin: EdgeInsets.zero,
                      child: ListTile(
                        leading: Icon(Icons.swipe, color: cs.primary),
                        title: Text(item),
                        trailing: const Icon(Icons.chevron_right),
                      ),
                    ),
                  ),
                ),
              );
            },
          ),
          if (_swipeItems.isEmpty)
            TextButton.icon(
              onPressed: () {
                setState(() {
                  _swipeItems.addAll(
                      List.generate(4, (i) => 'Tactile Card Item #${i + 1}'));
                  _swipeProgress.clear();
                  _swipeTrackers.clear();
                });
              },
              icon: const Icon(Icons.refresh),
              label: const Text('Reset Swipeable List'),
            ),
        ],
      ),
    );
  }

  Widget _buildPresetsSection(ColorScheme cs) {
    return _buildCardWrapper(
      title: 'Raw Composition Taps',
      subtitle: 'Fire standalone haptic compositions directly.',
      child: Wrap(
        spacing: 12,
        runSpacing: 8,
        children: [
          ActionChip(
            avatar: const Icon(Icons.touch_app),
            label: const Text('Subtle Tick (0.3)'),
            onPressed: () => applyTypedHaptic('tickCrossing', 0.3),
          ),
          ActionChip(
            avatar: const Icon(Icons.touch_app),
            label: const Text('Standard Tick (0.6)'),
            onPressed: () => applyTypedHaptic('tickCrossing', 0.6),
          ),
          ActionChip(
            avatar: const Icon(Icons.touch_app),
            label: const Text('Composed Drag burst'),
            onPressed: () => applyTypedHaptic('dragTexture', 0.7),
          ),
          ActionChip(
            avatar: const Icon(Icons.touch_app),
            label: const Text('Upper Bookend Click'),
            onPressed: () => applyTypedHaptic('bookendUpper', 1.0),
          ),
        ],
      ),
    );
  }

  Widget _buildCardWrapper({
    required String title,
    required String subtitle,
    required Widget child,
  }) {
    return Card(
      color: const Color(0xFF25232A),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title,
                style:
                    const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 4),
            Text(subtitle,
                style: const TextStyle(fontSize: 13, color: Colors.grey)),
            const SizedBox(height: 16),
            child,
          ],
        ),
      ),
    );
  }
}
1
likes
160
points
85
downloads

Documentation

API reference

Publisher

verified publishermuditpurohit.tech

Weekly Downloads

High-fidelity AOSP-aligned native haptic feedback compositions for Flutter.

Repository (GitHub)
View/report issues

Topics

#ui #material-3-expressive #haptics #feedback #aosp

License

MIT (license)

Dependencies

flutter

More

Packages that depend on m3e_haptics

Packages that implement m3e_haptics