rive_animation_manager 1.0.16
rive_animation_manager: ^1.0.16 copied to clipboard
A comprehensive Flutter package for managing Rive animations with data binding, image replacement, and global state management capabilities.
// ========================================
// RIVE ANIMATION MANAGER - INTERACTIVE EXAMPLE
// ========================================
// This example demonstrates:
// - Bidirectional data binding with Rive animations
// - Real-time property updates (Rive ↔ UI)
// - Type-specific interactive controls (string, number, boolean, color, trigger, enum)
// - Event logging and state management
// - Responsive side-by-side layout (desktop) / stacked layout (mobile)
// - Dynamic UI generation based on ViewModel properties
// ========================================
import 'package:flutter/material.dart';
import 'package:rive_native/rive_native.dart';
import 'package:rive_animation_manager/rive_animation_manager.dart';
import 'package:flutter/material.dart' as material_color;
void main() {
runApp(const MyApp());
}
/// Root application widget
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rive Animation Manager - Interactive',
theme: ThemeData(
primarySwatch: material_color.Colors.blue,
useMaterial3: true,
),
home: const RiveAnimationExample(),
);
}
}
/// Main example widget demonstrating interactive Rive animation controls
///
/// This widget showcases:
/// - Loading and displaying Rive animations with state machines
/// - Discovering ViewModel data binding properties automatically
/// - Creating type-specific UI controls for each property
/// - Handling bidirectional updates between UI and animation
class RiveAnimationExample extends StatefulWidget {
const RiveAnimationExample({super.key});
@override
State<RiveAnimationExample> createState() => _RiveAnimationExampleState();
}
class _RiveAnimationExampleState extends State<RiveAnimationExample> {
// ========================================
// STATE VARIABLES
// ========================================
/// Current animation status message displayed to user
String _currentStatus = 'Loading...';
/// List of discovered ViewModel properties from the Rive file
/// Each property contains: name, type, value, and property reference
List<Map<String, dynamic>> _properties = [];
/// Event log showing recent interactions and state changes
/// Limited to last 10 events for performance
final List<String> _eventLog = [];
/// Global controller instance for managing Rive animations
/// Used to update property values programmatically
final RiveAnimationController _controller = RiveAnimationController.instance;
// ========================================
// CALLBACK HANDLERS
// ========================================
// These callbacks are triggered by RiveManager at various lifecycle events
/// Called when the Rive animation is initialized and ready
///
/// @param artboard The artboard instance that was initialized
void _onInit(Artboard artboard) {
_addEventLog('✅ Animation Initialized: ${artboard.name}');
debugPrint('Animation initialized: ${artboard.name}');
}
/// Called when a state machine input changes (e.g., user clicks on animation)
///
/// @param index The index of the input that changed
/// @param name The name of the input
/// @param value The new value of the input
void _onInputChange(int index, String name, dynamic value) {
_addEventLog('📝 Input Changed: $name = $value');
}
/// Called when a hover action occurs on the animation
///
/// @param name The name of the hovered input
/// @param value The value associated with the hover action
void _onHoverAction(String name, dynamic value) {
_addEventLog('🎯 Hover Action: $name = $value');
}
/// Called when a trigger input is fired in the state machine
///
/// @param name The name of the trigger that was fired
/// @param value The trigger value (typically true)
void _onTriggerAction(String name, dynamic value) {
_addEventLog('⚡ Trigger Fired: $name');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_currentStatus = 'Trigger fired: $name';
});
}
});
}
/// Called when ViewModel data binding properties are discovered in the Rive file
/// This is where we receive the list of all bindable properties
///
/// @param properties List of discovered properties with metadata
void _onViewModelPropertiesDiscovered(List<Map<String, dynamic>> properties) {
if (properties.isEmpty) {
_addEventLog('⚠️ No ViewModel properties found');
} else {
_addEventLog('🔍 Discovered ${properties.length} properties');
}
// Update UI with discovered properties
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_properties = properties;
_currentStatus = properties.isEmpty
? 'No data binding properties'
: 'Discovered ${properties.length} properties';
});
}
});
}
/// Called when a data binding property changes value in the Rive animation
/// This enables Rive → UI updates (animation drives the UI)
///
/// @param propertyName The name of the property that changed
/// @param propertyType The type of the property (string, number, boolean, etc.)
/// @param value The new value of the property
void _onDataBindingChange(
String propertyName,
String propertyType,
dynamic value,
) {
_addEventLog('🔗 Property Updated: $propertyName = $value');
// Update the property value in our local list
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
for (int i = 0; i < _properties.length; i++) {
if (_properties[i]['name'] == propertyName) {
// Update the value while preserving other metadata
_properties[i] = {
..._properties[i],
'value': value,
};
break;
}
}
});
}
});
}
/// Called when a Rive event is fired from the state machine
///
/// @param eventName The name of the event
/// @param event The event object
/// @param currentState The current state machine state
void _onEventChange(String eventName, Event event, String currentState) {
_addEventLog('📢 Event: $eventName (State: $currentState)');
}
/// Called when an animation completes playing
void _onAnimationComplete() {
_addEventLog('✨ Animation Complete!');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_currentStatus = 'Animation completed!';
});
}
});
}
/// Helper method to add a message to the event log
/// Automatically limits log size to last 10 entries
///
/// @param message The message to add to the log
void _addEventLog(String message) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_eventLog.insert(0, message); // Add to beginning
if (_eventLog.length > 10) {
_eventLog.removeLast(); // Remove oldest entry
}
});
}
});
}
// ========================================
// USER INTERACTION HANDLERS
// ========================================
/// Called when user changes a property value via UI controls
/// This enables UI → Rive updates (UI drives the animation)
///
/// @param propertyName The name of the property to update
/// @param propertyType The type of the property
/// @param newValue The new value to set
Future<void> _onPropertyChanged(
String propertyName, String propertyType, dynamic newValue) async {
try {
_addEventLog('🎮 User Changed: $propertyName = $newValue');
// Send the update to the Rive animation via the controller
await _controller.updateDataBindingProperty(
'example_animation', // Animation ID (must match RiveManager animationId)
propertyName,
newValue,
);
debugPrint('Property updated: $propertyName = $newValue');
} catch (e) {
_addEventLog('❌ Error updating $propertyName: $e');
debugPrint('Error updating property: $e');
}
}
// ========================================
// BUILD METHOD
// ========================================
@override
Widget build(BuildContext context) {
// Determine screen size for responsive layout
final screenWidth = MediaQuery.of(context).size.width;
final isWideScreen = screenWidth > 900;
return Scaffold(
appBar: AppBar(
title: const Text('Rive Animation Manager - Interactive'),
elevation: 0,
),
// Use side-by-side layout for wide screens, stacked for narrow
body: isWideScreen ? _buildWideLayout() : _buildNarrowLayout(),
);
}
// ========================================
// LAYOUT: WIDE SCREEN (DESKTOP)
// ========================================
/// Builds side-by-side layout for desktop/wide screens
/// Left side: Rive animation (60% width)
/// Right side: Interactive controls panel (40% width)
Widget _buildWideLayout() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ====================================
// LEFT SIDE: Animation Display
// ====================================
Expanded(
flex: 6, // 60% of width
child: Container(
color: material_color.Colors.grey[100],
child: Column(
children: [
// Animation Area
Expanded(
child: Center(
child: RiveManager(
// Unique ID for this animation instance
animationId: 'example_animation',
// Path to your .riv file in assets
riveFilePath:
'assets/animations/data-change-on-click.riv',
// Use state machine for interactive animations
animationType: RiveAnimationType.stateMachine,
// Display properties
fit: Fit.contain,
alignment: Alignment.center,
// Callback handlers for various events
onInit: _onInit,
onInputChange: _onInputChange,
onHoverAction: _onHoverAction,
onTriggerAction: _onTriggerAction,
onViewModelPropertiesDiscovered:
_onViewModelPropertiesDiscovered,
onDataBindingChange: _onDataBindingChange,
onEventChange: _onEventChange,
onAnimationComplete: _onAnimationComplete,
),
),
),
// Status Bar at Bottom
Container(
width: double.infinity,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: material_color.Colors.white,
border: Border(
top: BorderSide(color: material_color.Colors.grey[300]!),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Status:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: material_color.Colors.grey,
),
),
const SizedBox(height: 4),
Text(
_currentStatus,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
// ====================================
// RIGHT SIDE: Controls Panel
// ====================================
Expanded(
flex: 4, // 40% of width
child: Container(
decoration: BoxDecoration(
color: material_color.Colors.white,
border: Border(
left: BorderSide(color: material_color.Colors.grey[300]!),
),
),
child: Column(
children: [
// Panel Header
Container(
width: double.infinity,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: material_color.Colors.blue[50],
border: Border(
bottom:
BorderSide(color: material_color.Colors.blue[100]!),
),
),
child: const Row(
children: [
Icon(Icons.tune, size: 20),
SizedBox(width: 8),
Text(
'Interactive Controls',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Scrollable Controls Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ====================================
// Property Controls Section
// ====================================
// This section dynamically generates UI controls
// based on discovered ViewModel properties
if (_properties.isEmpty)
// Show warning if no properties found
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: material_color.Colors.amber[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: material_color.Colors.amber[300]!),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'⚠️ No properties discovered',
style: TextStyle(
color: material_color.Colors.amber,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Make sure your Rive file has ViewModel data binding configured.',
style: TextStyle(
fontSize: 12,
color: material_color.Colors.grey),
),
],
),
)
else
// Generate a card for each property
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _properties.length,
itemBuilder: (context, index) {
final prop = _properties[index];
return InteractivePropertyCard(
property: prop,
onValueChanged: _onPropertyChanged,
);
},
),
const SizedBox(height: 24),
// ====================================
// Event Log Section
// ====================================
const Text(
'📋 Event Log:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
if (_eventLog.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: material_color.Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'No events yet - interact with animation!',
style:
TextStyle(color: material_color.Colors.grey),
),
)
else
Container(
constraints: const BoxConstraints(maxHeight: 200),
decoration: BoxDecoration(
border: Border.all(
color: material_color.Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
color: material_color.Colors.grey[50],
),
child: ListView.builder(
shrinkWrap: true,
itemCount: _eventLog.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Text(
_eventLog[index],
style: const TextStyle(
fontSize: 10,
fontFamily: 'Courier',
),
),
);
},
),
),
],
),
),
),
],
),
),
),
],
);
}
// ========================================
// LAYOUT: NARROW SCREEN (MOBILE/TABLET)
// ========================================
/// Builds stacked layout for mobile/narrow screens
/// Animation on top, controls below
Widget _buildNarrowLayout() {
return SingleChildScrollView(
child: Column(
children: [
// Animation Display Area
Container(
color: material_color.Colors.grey[100],
height: 300,
child: RiveManager(
animationId: 'example_animation',
riveFilePath: 'assets/animations/roportaj (5).riv',
animationType: RiveAnimationType.stateMachine,
fit: Fit.contain,
alignment: Alignment.center,
onInit: _onInit,
onInputChange: _onInputChange,
onHoverAction: _onHoverAction,
onTriggerAction: _onTriggerAction,
onViewModelPropertiesDiscovered: _onViewModelPropertiesDiscovered,
onDataBindingChange: _onDataBindingChange,
onEventChange: _onEventChange,
onAnimationComplete: _onAnimationComplete,
),
),
// Status Display
Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: material_color.Colors.grey,
),
),
const SizedBox(height: 8),
Text(
_currentStatus,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
// Interactive Property Controls
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.tune, size: 20),
SizedBox(width: 8),
Text(
'Interactive Property Controls',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
if (_properties.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: material_color.Colors.amber[50],
borderRadius: BorderRadius.circular(8),
border:
Border.all(color: material_color.Colors.amber[300]!),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'⚠️ No properties discovered',
style: TextStyle(
color: material_color.Colors.amber,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Make sure your Rive file has ViewModel data binding configured.',
style: TextStyle(
fontSize: 12, color: material_color.Colors.grey),
),
],
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _properties.length,
itemBuilder: (context, index) {
final prop = _properties[index];
return InteractivePropertyCard(
property: prop,
onValueChanged: _onPropertyChanged,
);
},
),
],
),
),
// Event Log
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'📋 Event Log:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (_eventLog.isNotEmpty)
TextButton(
onPressed: () {
setState(() {
_eventLog.clear();
});
},
child: const Text('Clear'),
),
],
),
const SizedBox(height: 12),
if (_eventLog.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: material_color.Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'No events yet - interact with animation!',
style: TextStyle(color: material_color.Colors.grey),
),
)
else
Container(
constraints: const BoxConstraints(maxHeight: 200),
decoration: BoxDecoration(
border:
Border.all(color: material_color.Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
color: material_color.Colors.grey[50],
),
child: ListView.builder(
shrinkWrap: true,
itemCount: _eventLog.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Text(
_eventLog[index],
style: const TextStyle(
fontSize: 11,
fontFamily: 'Courier',
),
),
);
},
),
),
],
),
),
const SizedBox(height: 24),
],
),
);
}
}
// ========================================
// INTERACTIVE PROPERTY CARD
// ========================================
/// Widget that displays a single property with type-specific controls
///
/// Automatically generates appropriate UI based on property type:
/// - string: TextField with send button
/// - number: Slider
/// - boolean: Switch
/// - color: Color palette buttons
/// - trigger: Fire button
/// - enumType: Dropdown selector
class InteractivePropertyCard extends StatefulWidget {
/// The property data containing name, type, and value
final Map<String, dynamic> property;
/// Callback when user changes the property value
final Function(String propertyName, String propertyType, dynamic newValue)
onValueChanged;
const InteractivePropertyCard({
super.key,
required this.property,
required this.onValueChanged,
});
@override
State<InteractivePropertyCard> createState() =>
_InteractivePropertyCardState();
}
class _InteractivePropertyCardState extends State<InteractivePropertyCard> {
@override
Widget build(BuildContext context) {
// Extract property metadata
final name = widget.property['name'] as String? ?? '';
final type = widget.property['type'] as String? ?? '';
final value = widget.property['value'];
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ====================================
// Header: Property name, value, and type badge
// ====================================
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Property name
Text(
name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// Current value display
Text(
_formatValue(value, type),
style: TextStyle(
fontSize: 12,
color: material_color.Colors.grey[600],
fontWeight: FontWeight.w600,
),
),
],
),
),
// Type badge with color coding
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getTypeColor(type),
borderRadius: BorderRadius.circular(12),
),
child: Text(
type,
style: const TextStyle(
fontSize: 10,
color: material_color.Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 12),
// ====================================
// Dynamic control based on property type
// ====================================
_buildControlForType(context, name, type, value),
],
),
),
);
}
/// Builds the appropriate UI control based on property type
Widget _buildControlForType(
BuildContext context, String name, String type, dynamic value) {
switch (type) {
case 'string':
return _buildStringControl(name, value);
case 'number':
return _buildNumberControl(name, value);
case 'boolean':
return _buildBooleanControl(name, value);
case 'color':
return _buildColorControl(name, value);
case 'trigger':
return _buildTriggerControl(name);
case 'enumType':
return _buildEnumControl(name, value);
default:
return const SizedBox.shrink();
}
}
// ====================================
// STRING CONTROL: TextField with send button
// ====================================
Widget _buildStringControl(String name, dynamic value) {
final controller = TextEditingController(text: value?.toString() ?? '');
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Enter text',
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
suffixIcon: IconButton(
icon: const Icon(Icons.send, size: 18),
onPressed: () {
// Send updated value to Rive
widget.onValueChanged(name, 'string', controller.text);
},
),
),
style: const TextStyle(fontSize: 13),
onSubmitted: (newValue) {
// Also send on Enter key
widget.onValueChanged(name, 'string', newValue);
},
);
}
// ====================================
// NUMBER CONTROL: Slider
// ====================================
Widget _buildNumberControl(String name, dynamic value) {
final numValue = (value is num) ? value.toDouble() : 0.0;
return Column(
children: [
Slider(
value: numValue.clamp(-1920, 1920), // Clamp to range
min: -1920,
max: 1920,
divisions: 294, // Granularity of slider steps
label: numValue.toStringAsFixed(1), // Show value as label
onChanged: (newValue) {
// Update Rive animation as user drags slider
widget.onValueChanged(name, 'number', newValue);
},
),
],
);
}
// ====================================
// BOOLEAN CONTROL: Switch with ON/OFF label
// ====================================
Widget _buildBooleanControl(String name, dynamic value) {
final boolValue = value == true;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
boolValue ? 'ON' : 'OFF',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: boolValue
? material_color.Colors.green
: material_color.Colors.grey,
),
),
Switch(
value: boolValue,
onChanged: (newValue) {
// Toggle boolean property
widget.onValueChanged(name, 'boolean', newValue);
},
),
],
);
}
// ====================================
// COLOR CONTROL: Palette of color buttons
// ====================================
Widget _buildColorControl(String name, dynamic value) {
return Wrap(
spacing: 6,
runSpacing: 6,
children: [
_colorButton(material_color.Colors.red, 'R', name),
_colorButton(material_color.Colors.green, 'G', name),
_colorButton(material_color.Colors.blue, 'B', name),
_colorButton(material_color.Colors.yellow, 'Y', name),
_colorButton(material_color.Colors.purple, 'P', name),
_colorButton(material_color.Colors.orange, 'O', name),
_colorButton(material_color.Colors.teal, 'T', name),
_colorButton(material_color.Colors.pink, 'Pi', name),
],
);
}
/// Helper: Creates a single color button
Widget _colorButton(Color color, String label, String propertyName) {
return InkWell(
onTap: () {
// Apply selected color to property
widget.onValueChanged(propertyName, 'color', color);
},
child: Container(
width: 40,
height: 32,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: material_color.Colors.grey[300]!, width: 1),
),
child: Center(
child: Text(
label,
style: const TextStyle(
fontSize: 9,
color: material_color.Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
// ====================================
// TRIGGER CONTROL: Fire button
// ====================================
Widget _buildTriggerControl(String name) {
return SizedBox(
width: double.infinity,
height: 36,
child: ElevatedButton.icon(
onPressed: () {
// Fire the trigger (send true value)
widget.onValueChanged(name, 'trigger', true);
},
icon: const Icon(Icons.play_arrow, size: 16),
label: Text('Fire: $name', style: const TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom(
backgroundColor: material_color.Colors.red,
foregroundColor: material_color.Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12),
),
),
);
}
// ====================================
// ENUM CONTROL: Dropdown selector
// ====================================
Widget _buildEnumControl(String name, dynamic value) {
// Example enum values - customize based on your Rive file
final enumOptions = ['Option 1', 'Option 2', 'Option 3'];
final currentValue = value?.toString() ?? enumOptions.first;
return DropdownButtonFormField<String>(
initialValue:
enumOptions.contains(currentValue) ? currentValue : enumOptions.first,
decoration: const InputDecoration(
labelText: 'Select option',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
style: const TextStyle(fontSize: 13, color: material_color.Colors.black),
items: enumOptions.map((option) {
return DropdownMenuItem<String>(
value: option,
child: Text(option),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
widget.onValueChanged(name, 'enumType', newValue);
}
},
);
}
// ====================================
// HELPER METHODS
// ====================================
/// Returns color for type badge based on property type
Color _getTypeColor(String type) {
switch (type) {
case 'string':
return material_color.Colors.blue;
case 'number':
return material_color.Colors.green;
case 'boolean':
return material_color.Colors.orange;
case 'color':
return material_color.Colors.purple;
case 'trigger':
return material_color.Colors.red;
case 'image':
return material_color.Colors.teal;
case 'enumType':
return material_color.Colors.indigo;
default:
return material_color.Colors.grey;
}
}
/// Formats property value for display
String _formatValue(dynamic value, String type) {
if (value == null) {
return type == 'trigger' ? 'Ready' : 'null';
}
if (value is double) {
return value.toStringAsFixed(2);
}
if (value is bool) {
return value ? 'true' : 'false';
}
if (value is String) {
return value.isEmpty ? '(empty)' : value;
}
return value.toString();
}
}