Skip to main content

flutter_orb 1.1.0 copy "flutter_orb: ^1.1.0" to clipboard
flutter_orb: ^1.1.0 copied to clipboard

High-performance, audio-reactive 3D particle voice orb visualizer in Flutter powered by Impeller-compatible runtime shaders and real-time microphone capture.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_orb/flutter_orb.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const VoiceOrbExampleApp());
}

class VoiceOrbExampleApp extends StatelessWidget {
  final bool autoStartTour;

  const VoiceOrbExampleApp({
    super.key,
    this.autoStartTour = false,
  });

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Orb Demo',
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.dark,
      darkTheme: ThemeData.dark(useMaterial3: true).copyWith(
        scaffoldBackgroundColor: const Color(0xFF08090C),
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF4318FF),
          brightness: Brightness.dark,
          surface: const Color(0xFF10131B),
        ),
      ),
      home: VoiceOrbHomePage(autoStartTour: autoStartTour),
    );
  }
}

class VoiceOrbHomePage extends StatefulWidget {
  final bool autoStartTour;

  const VoiceOrbHomePage({
    super.key,
    this.autoStartTour = false,
  });

  @override
  State<VoiceOrbHomePage> createState() => _VoiceOrbHomePageState();
}

class _VoiceOrbHomePageState extends State<VoiceOrbHomePage>
    with SingleTickerProviderStateMixin {
  late final VoiceOrbController _controller;
  late final AnimationController _tickerController;

  // Visual Customization State
  OrbStyle _currentStyle = OrbStyle.gemini();
  String _selectedPresetName = 'Gemini';
  final ScrollController _presetScrollController = ScrollController();

  bool _isCompactView = false;
  bool _showSettings = false;

  // Auto UI Tour Navigation State
  bool _isAutoTourActive = false;
  int _tourStep = 0;
  Timer? _tourTimer;
  String _tourDescription = '';

  final Map<String, OrbStyle> _presets = {
    'Gemini': OrbStyle.gemini(),
    'Cyberpunk': OrbStyle.cyberpunk(),
    'Solar Flare': OrbStyle.solar(),
    'Emerald': OrbStyle.emerald(),
    'Neon Rose': OrbStyle.neonRose(),
    'Monochrome': OrbStyle.monochrome(),
  };

  @override
  void initState() {
    super.initState();
    _controller = VoiceOrbController(
      smoothingFactor: 0.18,
      minDb: -55.0,
      maxDb: -5.0,
      powerBoost: 1.5,
    );

    _tickerController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )
      ..addListener(() {
        _controller.tick(0.016);
      })
      ..repeat();

    // Attempt start microphone; fallback to simulated speech if not granted
    _initVoiceCapture();

    // Listen for direct adb/intent test control commands
    const MethodChannel('com.halooid.orbView/control')
        .setMethodCallHandler((call) async {
      stopAutoTour();
      switch (call.method) {
        case 'setTourStep':
          final step = call.arguments as int;
          setState(() {
            _tourStep = step;
            _executeTourStep(step);
          });
          break;
        case 'setPreset':
          final rawName = (call.arguments as String).trim();
          final matchedKey = _presets.keys.firstWhere(
            (k) =>
                k.toLowerCase().replaceAll(' ', '') ==
                rawName.toLowerCase().replaceAll(' ', ''),
            orElse: () => _presets.containsKey(rawName) ? rawName : '',
          );
          if (matchedKey.isNotEmpty && _presets.containsKey(matchedKey)) {
            final idx = _presets.keys.toList().indexOf(matchedKey);
            setState(() {
              _selectedPresetName = matchedKey;
              _currentStyle = _presets[matchedKey]!;
              _isCompactView = false;
              _showSettings = false;
            });
            if (idx >= 0) _scrollToPreset(idx);
          }
          break;
        case 'setCompact':
          final compact = call.arguments as bool;
          setState(() {
            _isCompactView = compact;
          });
          break;
        case 'setSettings':
          final show = call.arguments as bool;
          setState(() {
            _showSettings = show;
          });
          break;
      }
    });

    if (widget.autoStartTour) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        startAutoTour();
      });
    }
  }

  Future<void> _initVoiceCapture() async {
    final granted = await _controller.start();
    if (!granted && mounted) {
      _controller.setSimulated(true, mode: SimulationMode.speech);
    }
  }

  void _scrollToPreset(int index) {
    if (!_presetScrollController.hasClients) return;
    final double targetOffset = (index * 130.0).clamp(
      0.0,
      _presetScrollController.position.maxScrollExtent,
    );
    _presetScrollController.animateTo(
      targetOffset,
      duration: const Duration(milliseconds: 350),
      curve: Curves.easeOutCubic,
    );
  }

  /// Starts the automatic UI tour navigation sequence.
  void startAutoTour() {
    setState(() {
      _isAutoTourActive = true;
      _tourStep = 0;
    });
    _executeTourStep(_tourStep);

    _tourTimer?.cancel();
    _tourTimer = Timer.periodic(const Duration(milliseconds: 3500), (timer) {
      if (!mounted || !_isAutoTourActive) {
        timer.cancel();
        return;
      }
      setState(() {
        _tourStep = (_tourStep + 1) % 8;
      });
      _executeTourStep(_tourStep);
    });
  }

  /// Stop or pause the automatic UI tour.
  void stopAutoTour() {
    _tourTimer?.cancel();
    _tourTimer = null;
    setState(() {
      _isAutoTourActive = false;
      _tourDescription = '';
    });
  }

  void _executeTourStep(int step) {
    _controller.setSimulated(true);

    switch (step) {
      case 0:
        setState(() {
          _selectedPresetName = 'Gemini';
          _currentStyle = _presets['Gemini']!;
          _isCompactView = false;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.speech;
          _tourDescription = '1/6: Google Gemini Aura';
        });
        _scrollToPreset(0);
        break;
      case 1:
        setState(() {
          _selectedPresetName = 'Cyberpunk';
          _currentStyle = _presets['Cyberpunk']!;
          _isCompactView = false;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.pulse;
          _tourDescription = '2/6: Cyberpunk Pulse';
        });
        _scrollToPreset(1);
        break;
      case 2:
        setState(() {
          _selectedPresetName = 'Solar Flare';
          _currentStyle = _presets['Solar Flare']!;
          _isCompactView = false;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.sine;
          _tourDescription = '3/6: Solar Flare Dynamics';
        });
        _scrollToPreset(2);
        break;
      case 3:
        setState(() {
          _selectedPresetName = 'Emerald';
          _currentStyle = _presets['Emerald']!;
          _isCompactView = false;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.speech;
          _tourDescription = '4/6: Emerald Deep Mint';
        });
        _scrollToPreset(3);
        break;
      case 4:
        setState(() {
          _selectedPresetName = 'Neon Rose';
          _currentStyle = _presets['Neon Rose']!;
          _isCompactView = false;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.pulse;
          _tourDescription = '5/6: Neon Rose Hot Pink';
        });
        _scrollToPreset(4);
        break;
      case 5:
        setState(() {
          _selectedPresetName = 'Monochrome';
          _currentStyle = _presets['Monochrome']!;
          _isCompactView = false;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.speech;
          _tourDescription = '6/6: Monochrome Minimalist';
        });
        _scrollToPreset(5);
        break;
      case 6:
        setState(() {
          _selectedPresetName = 'Emerald';
          _currentStyle = _presets['Emerald']!;
          _isCompactView = true;
          _showSettings = false;
          _controller.simulationMode = SimulationMode.speech;
          _tourDescription = 'Feature: Compact Assistant Bubble';
        });
        _scrollToPreset(3);
        break;
      case 7:
        setState(() {
          _selectedPresetName = 'Neon Rose';
          _currentStyle = _presets['Neon Rose']!;
          _isCompactView = false;
          _showSettings = true;
          _controller.simulationMode = SimulationMode.pulse;
          _tourDescription = 'Feature: Live Shader & Audio Tuning';
        });
        _scrollToPreset(4);
        break;
    }
  }

  @override
  void dispose() {
    _presetScrollController.dispose();
    _tourTimer?.cancel();
    _tickerController.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // Background Gradient Glow
          Positioned.fill(
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 500),
              decoration: BoxDecoration(
                gradient: RadialGradient(
                  center: Alignment.center,
                  radius: 1.2,
                  colors: [
                    _currentStyle.silentColor.withValues(alpha: 0.18),
                    const Color(0xFF07080B),
                  ],
                ),
              ),
            ),
          ),

          // Main Interactive Layout
          SafeArea(
            child: Column(
              children: [
                _buildHeader(),
                if (_isAutoTourActive) _buildTourBanner(),
                Expanded(
                  child: Center(
                    child: _buildOrbStage(),
                  ),
                ),
                _buildMetricsBar(),
                _buildPresetSelector(),
                _buildControlBar(),
              ],
            ),
          ),

          // Tweak Settings Bottom Sheet
          if (_showSettings)
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: _buildSettingsSheet(),
            ),
        ],
      ),
    );
  }

  Widget _buildHeader() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Row(
            children: [
              Container(
                width: 10,
                height: 10,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: _controller.isListening
                      ? (_controller.isSimulated
                          ? Colors.amberAccent
                          : Colors.greenAccent)
                      : Colors.redAccent,
                  boxShadow: [
                    BoxShadow(
                      color: (_controller.isSimulated
                              ? Colors.amberAccent
                              : Colors.greenAccent)
                          .withValues(alpha: 0.6),
                      blurRadius: 8,
                      spreadRadius: 2,
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 10),
              Text(
                'FLUTTER ORB',
                style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.w800,
                  letterSpacing: 2.0,
                  color: Colors.white.withValues(alpha: 0.9),
                ),
              ),
            ],
          ),
          Row(
            children: [
              IconButton.filledTonal(
                key: const ValueKey('btn_auto_tour'),
                icon: Icon(_isAutoTourActive
                    ? Icons.pause_circle_filled
                    : Icons.auto_awesome),
                tooltip: _isAutoTourActive
                    ? 'Pause Auto Tour'
                    : 'Start Auto UI Tour',
                onPressed: () {
                  if (_isAutoTourActive) {
                    stopAutoTour();
                  } else {
                    startAutoTour();
                  }
                },
              ),
              const SizedBox(width: 8),
              IconButton.filledTonal(
                key: const ValueKey('btn_compact'),
                icon: Icon(
                    _isCompactView ? Icons.fullscreen : Icons.fullscreen_exit),
                tooltip: _isCompactView
                    ? 'Fullscreen Stage'
                    : 'Compact Assistant Bubble',
                onPressed: () {
                  setState(() {
                    _isCompactView = !_isCompactView;
                  });
                },
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildTourBanner() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
      decoration: BoxDecoration(
        color: _currentStyle.activeColor.withValues(alpha: 0.15),
        borderRadius: BorderRadius.circular(12),
        border:
            Border.all(color: _currentStyle.activeColor.withValues(alpha: 0.4)),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Row(
            children: [
              Icon(Icons.play_circle_fill,
                  size: 16, color: _currentStyle.activeColor),
              const SizedBox(width: 8),
              Text(
                _tourDescription,
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.bold,
                  color: _currentStyle.activeColor,
                ),
              ),
            ],
          ),
          InkWell(
            onTap: stopAutoTour,
            child: const Icon(Icons.close, size: 16, color: Colors.white70),
          ),
        ],
      ),
    );
  }

  Widget _buildOrbStage() {
    return LayoutBuilder(
      builder: (context, constraints) {
        final double fullWidth = constraints.maxWidth;
        final double fullHeight = constraints.maxHeight;
        final double width = _isCompactView ? 200.0 : fullWidth;
        final double height = _isCompactView ? 200.0 : fullHeight;

        return AnimatedContainer(
          duration: const Duration(milliseconds: 400),
          curve: Curves.easeInOutCubic,
          width: width,
          height: height,
          decoration: _isCompactView
              ? BoxDecoration(
                  shape: BoxShape.circle,
                  color: Colors.black.withValues(alpha: 0.6),
                  border: Border.all(
                    color: _currentStyle.activeColor.withValues(alpha: 0.4),
                    width: 1.5,
                  ),
                  boxShadow: [
                    BoxShadow(
                      color: _currentStyle.silentColor.withValues(alpha: 0.3),
                      blurRadius: 30,
                      spreadRadius: 5,
                    ),
                  ],
                )
              : const BoxDecoration(
                  color: Colors.transparent,
                ),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(_isCompactView ? 100 : 0),
            child: ParticleOrb(
              energyListenable: _controller,
              style: _currentStyle,
            ),
          ),
        );
      },
    );
  }

  Widget _buildMetricsBar() {
    return ListenableBuilder(
      listenable: _controller,
      builder: (context, _) {
        final db = _controller.currentDb;
        final energy = _controller.energy;
        final modeLabel = _controller.isSimulated
            ? 'Simulation (${_controller.simulationMode.name.toUpperCase()})'
            : (_controller.isListening ? 'LIVE MICROPHONE' : 'STANDBY');

        return Container(
          margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          decoration: BoxDecoration(
            color: const Color(0xFF141722).withValues(alpha: 0.85),
            borderRadius: BorderRadius.circular(16),
            border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
          ),
          child: Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Expanded(
                    child: Text(
                      modeLabel,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.bold,
                        letterSpacing: 1.1,
                        color: _controller.isSimulated
                            ? Colors.amberAccent
                            : Colors.cyanAccent,
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    '${db.toStringAsFixed(1)} dBFS  |  ${(energy * 100).toInt()}%',
                    maxLines: 1,
                    style: TextStyle(
                      fontSize: 12,
                      fontFamily: 'Courier',
                      fontWeight: FontWeight.bold,
                      color: Colors.white.withValues(alpha: 0.8),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              ClipRRect(
                borderRadius: BorderRadius.circular(6),
                child: LinearProgressIndicator(
                  value: energy.clamp(0.0, 1.0),
                  minHeight: 6,
                  backgroundColor: Colors.white.withValues(alpha: 0.06),
                  valueColor:
                      AlwaysStoppedAnimation<Color>(_currentStyle.activeColor),
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildPresetSelector() {
    return Container(
      height: 44,
      margin: const EdgeInsets.symmetric(vertical: 6),
      child: ListView(
        controller: _presetScrollController,
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        children: _presets.keys.map((name) {
          final isSelected = _selectedPresetName == name;
          final preset = _presets[name]!;
          return Padding(
            padding: const EdgeInsets.only(right: 8),
            child: ChoiceChip(
              key: ValueKey('preset_$name'),
              avatar: Container(
                width: 12,
                height: 12,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  gradient: LinearGradient(
                    colors: [preset.silentColor, preset.activeColor],
                  ),
                ),
              ),
              label: Text(name),
              selected: isSelected,
              selectedColor: _currentStyle.activeColor.withValues(alpha: 0.2),
              side: BorderSide(
                color: isSelected
                    ? _currentStyle.activeColor
                    : Colors.white.withValues(alpha: 0.1),
                width: isSelected ? 1.5 : 1.0,
              ),
              onSelected: (selected) {
                if (selected) {
                  setState(() {
                    _selectedPresetName = name;
                    _currentStyle = preset;
                  });
                }
              },
            ),
          );
        }).toList(),
      ),
    );
  }

  Widget _buildControlBar() {
    return Container(
      padding: const EdgeInsets.fromLTRB(20, 10, 20, 16),
      child: Row(
        children: [
          Expanded(
            child: OutlinedButton.icon(
              key: const ValueKey('btn_mode_toggle'),
              style: OutlinedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 14),
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(14)),
                side: BorderSide(
                  color: _controller.isSimulated
                      ? Colors.amberAccent
                      : Colors.cyanAccent,
                ),
              ),
              icon: Icon(
                _controller.isSimulated ? Icons.graphic_eq : Icons.mic,
                color: _controller.isSimulated
                    ? Colors.amberAccent
                    : Colors.cyanAccent,
              ),
              label: Text(
                _controller.isSimulated ? 'Mode: Simulated' : 'Mode: Live Mic',
                style: const TextStyle(fontWeight: FontWeight.bold),
              ),
              onPressed: () {
                setState(() {
                  if (_controller.isSimulated) {
                    // Switch to live mic
                    _controller.setSimulated(false);
                    _controller.start();
                  } else {
                    // Switch to simulation
                    _controller.setSimulated(true, mode: SimulationMode.speech);
                  }
                });
              },
            ),
          ),
          if (_controller.isSimulated) ...[
            const SizedBox(width: 8),
            PopupMenuButton<SimulationMode>(
              key: const ValueKey('btn_sim_menu'),
              initialValue: _controller.simulationMode,
              icon: const Icon(Icons.tune),
              tooltip: 'Simulation Pattern',
              onSelected: (mode) {
                setState(() {
                  _controller.simulationMode = mode;
                });
              },
              itemBuilder: (context) => [
                const PopupMenuItem(
                  value: SimulationMode.speech,
                  child: Text('Conversational Speech'),
                ),
                const PopupMenuItem(
                  value: SimulationMode.pulse,
                  child: Text('Rhythmic Pulse'),
                ),
                const PopupMenuItem(
                  value: SimulationMode.sine,
                  child: Text('Sinusoidal Wave'),
                ),
                const PopupMenuItem(
                  value: SimulationMode.idle,
                  child: Text('Idle Beat'),
                ),
              ],
            ),
          ],
          const SizedBox(width: 8),
          IconButton.filledTonal(
            key: const ValueKey('btn_settings'),
            icon: Icon(_showSettings ? Icons.close : Icons.tune_rounded),
            tooltip: 'Fine-tune Visuals & Sensitivity',
            onPressed: () {
              setState(() {
                _showSettings = !_showSettings;
              });
            },
          ),
        ],
      ),
    );
  }

  Widget _buildSettingsSheet() {
    return Container(
      constraints: const BoxConstraints(maxHeight: 380),
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: const Color(0xFF131620),
        borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.6),
            blurRadius: 20,
            spreadRadius: 4,
          ),
        ],
      ),
      child: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'Shader & Audio Tuning',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                IconButton(
                  icon: const Icon(Icons.close, size: 20),
                  onPressed: () => setState(() => _showSettings = false),
                ),
              ],
            ),
            const Divider(height: 20),
            _buildSlider(
              label: 'Base Core Radius',
              value: _currentStyle.baseRadius,
              min: 0.10,
              max: 0.35,
              onChanged: (v) => setState(() {
                _currentStyle = _currentStyle.copyWith(baseRadius: v);
              }),
            ),
            _buildSlider(
              label: 'Plasma Glow Intensity',
              value: _currentStyle.glowIntensity,
              min: 0.3,
              max: 2.5,
              onChanged: (v) => setState(() {
                _currentStyle = _currentStyle.copyWith(glowIntensity: v);
              }),
            ),
            _buildSlider(
              label: 'Turbulence Speed Multiplier',
              value: _currentStyle.speedMultiplier,
              min: 0.2,
              max: 2.5,
              onChanged: (v) => setState(() {
                _currentStyle = _currentStyle.copyWith(speedMultiplier: v);
              }),
            ),
            _buildSlider(
              label: 'Idle Turbulence (0 = Perfect Sphere)',
              value: _currentStyle.idleTurbulence,
              min: 0.0,
              max: 1.0,
              onChanged: (v) => setState(() {
                _currentStyle = _currentStyle.copyWith(idleTurbulence: v);
              }),
            ),
            _buildSlider(
              label: 'Particle Dot Size',
              value: _currentStyle.particleSize,
              min: 0.8,
              max: 3.5,
              onChanged: (v) => setState(() {
                _currentStyle = _currentStyle.copyWith(particleSize: v);
              }),
            ),
            _buildSlider(
              label: '3D Wave Amplitude',
              value: _currentStyle.waveAmplitude,
              min: 0.04,
              max: 0.30,
              onChanged: (v) => setState(() {
                _currentStyle = _currentStyle.copyWith(waveAmplitude: v);
              }),
            ),
            _buildSlider(
              label: 'Decibel Floor (minDb)',
              value: _controller.minDb,
              min: -80.0,
              max: -25.0,
              onChanged: (v) => setState(() {
                _controller.minDb = v;
              }),
            ),
            _buildSlider(
              label: 'Smoothing Factor (EMA)',
              value: _controller.smoothingFactor,
              min: 0.05,
              max: 0.50,
              onChanged: (v) => setState(() {
                _controller.smoothingFactor = v;
              }),
            ),
            _buildSlider(
              label: 'Audio Power Boost Curve',
              value: _controller.powerBoost,
              min: 1.0,
              max: 2.5,
              onChanged: (v) => setState(() {
                _controller.powerBoost = v;
              }),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSlider({
    required String label,
    required double value,
    required double min,
    required double max,
    required ValueChanged<double> onChanged,
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          Expanded(
            flex: 4,
            child: Text(
              label,
              style: TextStyle(
                  fontSize: 12, color: Colors.white.withValues(alpha: 0.8)),
            ),
          ),
          Expanded(
            flex: 5,
            child: SliderTheme(
              data: SliderTheme.of(context).copyWith(
                trackHeight: 3,
                thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
              ),
              child: Slider(
                value: value.clamp(min, max),
                min: min,
                max: max,
                onChanged: onChanged,
              ),
            ),
          ),
          SizedBox(
            width: 42,
            child: Text(
              value.toStringAsFixed(2),
              textAlign: TextAlign.right,
              style: const TextStyle(fontSize: 11, fontFamily: 'Courier'),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
150
points
431
downloads

Documentation

API reference

Publisher

verified publisherjerinjimmy.com

Weekly Downloads

High-performance, audio-reactive 3D particle voice orb visualizer in Flutter powered by Impeller-compatible runtime shaders and real-time microphone capture.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, record

More

Packages that depend on flutter_orb