Flutter for OpenHarmony实现高性能流体粒子模拟:从物理引擎到交互式可视化

Flutter for OpenHarmony实现高性能流体粒子模拟:从物理引擎到交互式可视化

在移动图形编程领域,实时粒子系统一直是衡量框架性能与开发灵活性的重要标杆。本文将深入剖析一段完整的 Flutter

代码,展示如何构建一个支持触摸交互、动态参数调节和视觉特效 的流体粒子模拟应用。这不仅是一次对 Flutter

自定义绘制能力的极致探索,更是对经典物理模拟思想的现代实践。


完整效果展示


一、整体架构设计

1. 三层核心结构

  • 数据层Particle 类封装粒子状态(位置、速度、质量、颜色);
  • 逻辑层_FluidSimulationState 管理全局物理规则(重力、阻尼)与交互逻辑;
  • 渲染层FluidPainter 负责高性能绘制(粒子、连接线、力场范围)。

💡 这种分层实现了关注点分离------修改物理规则不影响渲染代码,调整视觉效果无需触碰核心逻辑。

2. 实时动画驱动

dart 复制代码
_controller = AnimationController(vsync: this, duration: 16ms)..repeat();
_controller.addListener(() {
  // 每帧更新所有粒子
  for (var particle in _particles) particle.update(...);
  setState(() {}); // 触发重绘
});
  • 60 FPS 流畅体验:16ms 帧间隔匹配屏幕刷新率;
  • 高效状态管理 :避免 setState 在循环内调用,仅在帧结束时触发一次重绘。

二、粒子物理系统详解

1. 基础运动学模型

每个粒子遵循经典牛顿力学:

dart 复制代码
void update(double gravity, double dampening, Size size) {
  vy += gravity * mass;    // 重力加速度(质量越大下落越快)
  x += vx; y += vy;        // 位置更新
  vx *= dampening;         // 空气阻力(速度衰减)
  vy *= dampening;
  
  // 边界碰撞(弹性系数 0.8)
  if (x < 0 || x > size.width) {
    vx *= -0.8; 
    x = x.clamp(0, size.width);
  }
}
  • 质量差异化mass = Random().nextDouble() * 2 + 1 使粒子运动更自然;
  • 非完全弹性碰撞:能量损失模拟真实物理世界。

2. 触摸交互力场

当用户触摸屏幕时,创建动态力场影响附近粒子:

dart 复制代码
// 计算粒子与触摸点的距离
final distance = sqrt(dx*dx + dy*dy);
if (distance < _interactionRadius) {
  final forceFactor = (1 - distance / _interactionRadius); // 距离衰减
  final force = (_isAttractMode ? 1 : -1) * forceFactor² * _forceMultiplier;
  
  // 应用力(F=ma → a=F/m)
  particle.vx += cos(angle) * force / particle.mass;
  particle.vy += sin(angle) * force / particle.mass;
}
  • 平方反比衰减forceFactor² 使力场边缘更平滑;
  • 质量归一化:轻粒子响应更灵敏,重粒子更稳定。

三、高级视觉特效实现

1. 动态发光粒子

dart 复制代码
// 根据速度混合颜色
final speedFactor = (speed / 10).clamp(0, 1);
final color = Color.lerp(particle.baseColor, Colors.white, speedFactor * 0.7)!;

// 绘制光晕(仅高速粒子)
if (speedFactor > 0.3) {
  final glowPaint = Paint()
    ..color = color.withValues(alpha: speedFactor * 0.3)
    ..maskFilter = MaskFilter.blur(BlurStyle.normal, 10);
  canvas.drawCircle(Offset(x,y), mass*2, glowPaint);
}
  • 速度感知:粒子越快越亮白,静止时保持原始色相;
  • 性能优化 :仅当 speedFactor > 0.3 时绘制光晕,避免过度绘制。

2. 粒子连接网络

dart 复制代码
if (showConnections) {
  for (int i=0; i<particles.length; i++) {
    for (int j=i+1; j<particles.length; j++) {
      if (distance < 50) {
        final alpha = 1 - (distance / 50); // 距离越近连线越亮
        connectionPaint.color = Colors.white.withValues(alpha: alpha * 0.2);
        canvas.drawLine(p1, p2, connectionPaint);
      }
    }
  }
}
  • O(n²) 优化:通过距离阈值(50px)大幅减少连线数量;
  • 半透明渐变:近距离连线更明显,远距离近乎隐形。

3. 交互力场可视化

dart 复制代码
// 径向渐变力场
final gradient = RadialGradient(
  colors: [modeColor.withAlpha(77), modeColor.withAlpha(0)],
  radius: interactionRadius / size.width,
);
canvas.drawCircle(touchPoint, interactionRadius, fieldPaint);

// 边界圆环
final ringPaint = Paint()..color = modeColor.withAlpha(128);
canvas.drawCircle(touchPoint, interactionRadius, ringPaint);
  • 模式区分:吸引模式(蓝色)、排斥模式(红色);
  • 深度感营造:内实外虚的渐变模拟力场强度衰减。

四、交互控制面板设计

1. 双模式切换

模式 图标 行为 视觉反馈
吸引 ↓ 圆箭头 粒子向触摸点聚集 蓝色高亮
排斥 ↑ 圆箭头 粒子远离触摸点 红色高亮

2. 视觉增强开关

  • 轨迹模式 :降低粒子透明度(alpha: 0.3),形成运动残影;
  • 连接模式:显示粒子间动态连线,揭示群体行为模式。

3. 深度设置系统

通过 AlertDialog 提供六维参数调节:

参数 范围 物理意义
粒子数量 50-500 系统复杂度
重力 -0.5~0.5 垂直加速度方向/强度
阻尼 0.9-1.0 能量耗散速率
作用半径 50-400px 力场影响范围
力的大小 0.1-2.0 交互强度

⚠️ 注意:StatefulBuilder 确保滑块拖动时实时预览参数变化,但仅在点击"应用"后更新主界面。


五、性能优化策略

1. 绘制效率保障

  • 条件绘制showTrails/showConnections 开关避免无效计算;
  • 智能重绘shouldRepaint 仅当关键参数变化时触发重绘;
  • Canvas 批处理 :单次 paint 调用完成所有绘制操作。

2. 内存管理

  • 对象复用 :粒子列表 _particles 初始化后不再重建;
  • 控制器释放dispose() 中清理 AnimationController

3. 坐标转换优化

dart 复制代码
// 高效获取局部坐标
final renderBox = context.findRenderObject() as RenderBox?;
_touchPoint = renderBox!.globalToLocal(details.globalPosition);

避免使用 MediaQuery 在每帧中重复计算。


六、潜在扩展方向

  1. 3D 粒子系统 结合 flutter_cubethree_dart 实现 Z 轴深度。

  2. 流体动力学 引入 Navier-Stokes 方程模拟真实流体(需 GPU 加速)。

  3. 粒子生命周期 添加生成/消亡机制,支持喷泉、爆炸等特效。

  4. WebGL 导出 通过 flutter_web_plugins 将 Canvas 转换为 WebGL 渲染。


结语:Flutter 的图形潜力

这个粒子模拟项目证明了 Flutter 不仅能构建精美 UI,更能胜任高性能图形计算 任务。通过合理利用 CustomPainterAnimationController 和 Dart 的数值计算能力,我们实现了:

  • 60 FPS 流畅动画
  • 复杂物理交互
  • 动态视觉特效

🌐 加入社区

欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持:

👉 开源鸿蒙跨平台开发者社区
完整代码展示

bash 复制代码
import 'dart:math';
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '流体粒子模拟',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.dark,
        ),
      ),
      home: const FluidSimulation(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class Particle {
  double x, y;
  double vx, vy;
  double mass;
  Color baseColor;
  double hue;

  Particle(this.x, this.y)
      : vx = (Random().nextDouble() - 0.5) * 2,
        vy = (Random().nextDouble() - 0.5) * 2,
        mass = Random().nextDouble() * 2 + 1,
        hue = Random().nextDouble() * 360,
        baseColor =
            HSLColor.fromAHSL(1.0, Random().nextDouble() * 360, 0.8, 0.6)
                .toColor();

  void update(double gravity, double dampening, Size size) {
    // 应用重力
    vy += gravity * mass;

    // 更新位置
    x += vx;
    y += vy;

    // 阻尼(空气阻力)
    vx *= dampening;
    vy *= dampening;

    // 边界碰撞检测 (左右)
    if (x < 0 || x > size.width) {
      vx *= -0.8;
      x = x.clamp(0, size.width);
    }

    // 边界碰撞检测 (上下)
    if (y < 0 || y > size.height) {
      vy *= -0.8;
      y = y.clamp(0, size.height);
    }

    // 颜色缓慢变化
    hue = (hue + 0.5) % 360;
    baseColor = HSLColor.fromAHSL(1.0, hue, 0.8, 0.6).toColor();
  }

  double getSpeed() {
    return sqrt(vx * vx + vy * vy);
  }
}

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

  @override
  State<FluidSimulation> createState() => _FluidSimulationState();
}

class _FluidSimulationState extends State<FluidSimulation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  List<Particle> _particles = [];
  Offset _touchPoint = Offset.zero;
  bool _isTouching = false;
  bool _isAttractMode = true;
  double _gravity = 0.1;
  double _dampening = 0.99;
  double _interactionRadius = 200;
  double _forceMultiplier = 0.8;
  int _particleCount = 300;
  bool _showTrails = false;
  bool _showConnections = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 16),
    )..repeat();

    _initializeParticles();

    _controller.addListener(() {
      final size = MediaQuery.of(context).size;

      // 更新每个粒子的状态
      for (var particle in _particles) {
        particle.update(_gravity, _dampening, size);

        // 处理触摸交互
        if (_isTouching) {
          final dx = _touchPoint.dx - particle.x;
          final dy = _touchPoint.dy - particle.y;
          final distance = sqrt(dx * dx + dy * dy);

          if (distance < _interactionRadius) {
            // 计算力的方向
            final angle = atan2(dy, dx);
            // 力的大小与距离成反比,使用平滑衰减
            final forceFactor = (1 - distance / _interactionRadius);
            final force = (_isAttractMode ? 1 : -1) *
                forceFactor *
                forceFactor *
                _forceMultiplier;

            particle.vx += cos(angle) * force / particle.mass;
            particle.vy += sin(angle) * force / particle.mass;
          }
        }
      }
      setState(() {});
    });
  }

  void _initializeParticles() {
    _particles = List.generate(
      _particleCount,
      (index) => Particle(
        Random().nextDouble() * 400,
        Random().nextDouble() * 400,
      ),
    );
  }

  void _resetParticles() {
    setState(() {
      _initializeParticles();
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

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

    return Scaffold(
      backgroundColor: const Color(0xFF0A0E1A),
      appBar: AppBar(
        title: const Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.water_drop, size: 24),
            SizedBox(width: 8),
            Text('流体粒子模拟', style: TextStyle(fontSize: 18)),
          ],
        ),
        centerTitle: true,
        elevation: 0,
        backgroundColor: Colors.transparent,
        titleSpacing: 8,
        toolbarHeight: kToolbarHeight,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh, size: 24),
            onPressed: _resetParticles,
            tooltip: '重置粒子',
            padding: const EdgeInsets.all(12),
            constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
          ),
          IconButton(
            icon: const Icon(Icons.settings, size: 24),
            onPressed: () => _showSettingsDialog(context),
            tooltip: '设置',
            padding: const EdgeInsets.all(12),
            constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
          ),
        ],
      ),
      body: Stack(
        children: [
          // 粒子画布
          Positioned.fill(
            child: GestureDetector(
              onPanStart: (details) {
                setState(() {
                  _isTouching = true;
                  final renderBox = context.findRenderObject() as RenderBox?;
                  if (renderBox != null) {
                    _touchPoint =
                        renderBox.globalToLocal(details.globalPosition);
                  }
                });
              },
              onPanUpdate: (details) {
                final renderBox = context.findRenderObject() as RenderBox?;
                if (renderBox != null) {
                  _touchPoint = renderBox.globalToLocal(details.globalPosition);
                }
              },
              onPanEnd: (details) {
                setState(() {
                  _isTouching = false;
                });
              },
              child: CustomPaint(
                size: Size.infinite,
                painter: FluidPainter(
                  _particles,
                  _touchPoint,
                  _isTouching,
                  _isAttractMode,
                  _interactionRadius,
                  _showTrails,
                  _showConnections,
                ),
              ),
            ),
          ),

          // 交互模式切换
          Positioned(
            bottom: 16,
            left: 0,
            right: 0,
            child: SafeArea(
              top: false,
              minimum: const EdgeInsets.only(bottom: 8),
              child: _buildControlPanel(theme),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildControlPanel(ThemeData theme) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 16),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
      decoration: BoxDecoration(
        color: theme.colorScheme.surface.withValues(alpha: 0.9),
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.3),
            blurRadius: 20,
            offset: const Offset(0, 4),
          ),
        ],
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Expanded(
            child: _buildModeButton(
              icon: Icons.arrow_circle_down,
              label: '吸引',
              isActive: _isAttractMode,
              color: Colors.blue,
              onTap: () => setState(() => _isAttractMode = true),
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: _buildModeButton(
              icon: Icons.arrow_circle_up,
              label: '排斥',
              isActive: !_isAttractMode,
              color: Colors.red,
              onTap: () => setState(() => _isAttractMode = false),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: _buildToggleChip(
              icon: Icons.blur_on,
              label: '轨迹',
              isActive: _showTrails,
              onTap: () => setState(() => _showTrails = !_showTrails),
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: _buildToggleChip(
              icon: Icons.hub,
              label: '连接',
              isActive: _showConnections,
              onTap: () => setState(() => _showConnections = !_showConnections),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildModeButton({
    required IconData icon,
    required String label,
    required bool isActive,
    required Color color,
    required VoidCallback onTap,
  }) {
    return Material(
      color: isActive ? color : Colors.grey[800],
      borderRadius: BorderRadius.circular(12),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(icon,
                  color: isActive ? Colors.white : Colors.white70, size: 22),
              const SizedBox(width: 6),
              Text(
                label,
                style: TextStyle(
                  color: isActive ? Colors.white : Colors.white70,
                  fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
                  fontSize: 14,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildToggleChip({
    required IconData icon,
    required String label,
    required bool isActive,
    required VoidCallback onTap,
  }) {
    return FilterChip(
      label: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 16, color: isActive ? Colors.white : Colors.white70),
          const SizedBox(width: 4),
          Text(
            label,
            style: TextStyle(
              color: isActive ? Colors.white : Colors.white70,
              fontSize: 12,
            ),
          ),
        ],
      ),
      selected: isActive,
      onSelected: (_) => onTap(),
      selectedColor: Colors.purple,
      backgroundColor: Colors.grey[800],
      checkmarkColor: Colors.white,
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
      visualDensity: VisualDensity.compact,
    );
  }

  void _showSettingsDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => StatefulBuilder(
        builder: (context, setDialogState) => AlertDialog(
          title: const Row(
            children: [
              Icon(Icons.tune, color: Colors.purple),
              SizedBox(width: 12),
              Text('设置'),
            ],
          ),
          content: SizedBox(
            width: double.maxFinite,
            child: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _buildSliderSetting(
                    label: '粒子数量: $_particleCount',
                    value: _particleCount.toDouble(),
                    min: 50,
                    max: 500,
                    divisions: 45,
                    onChanged: (value) {
                      setDialogState(() => _particleCount = value.toInt());
                    },
                  ),
                  _buildSliderSetting(
                    label: '重力: ${_gravity.toStringAsFixed(2)}',
                    value: _gravity,
                    min: -0.5,
                    max: 0.5,
                    divisions: 100,
                    onChanged: (value) {
                      setDialogState(() => _gravity = value);
                    },
                  ),
                  _buildSliderSetting(
                    label: '阻尼: ${_dampening.toStringAsFixed(3)}',
                    value: _dampening,
                    min: 0.9,
                    max: 1.0,
                    divisions: 100,
                    onChanged: (value) {
                      setDialogState(() => _dampening = value);
                    },
                  ),
                  _buildSliderSetting(
                    label: '作用半径: ${_interactionRadius.toInt()}',
                    value: _interactionRadius,
                    min: 50,
                    max: 400,
                    divisions: 70,
                    onChanged: (value) {
                      setDialogState(() => _interactionRadius = value);
                    },
                  ),
                  _buildSliderSetting(
                    label: '力的大小: ${_forceMultiplier.toStringAsFixed(1)}',
                    value: _forceMultiplier,
                    min: 0.1,
                    max: 2.0,
                    divisions: 19,
                    onChanged: (value) {
                      setDialogState(() => _forceMultiplier = value);
                    },
                  ),
                ],
              ),
            ),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  // 应用设置
                });
                Navigator.pop(context);
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.purple,
                foregroundColor: Colors.white,
              ),
              child: const Text('应用'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSliderSetting({
    required String label,
    required double value,
    required double min,
    required double max,
    required int divisions,
    required ValueChanged<double> onChanged,
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            label,
            style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
          ),
          const SizedBox(height: 4),
          Slider(
            value: value,
            min: min,
            max: max,
            divisions: divisions,
            label: label.split(': ').last,
            onChanged: onChanged,
          ),
        ],
      ),
    );
  }
}

class FluidPainter extends CustomPainter {
  final List<Particle> particles;
  final Offset touchPoint;
  final bool isTouching;
  final bool isAttractMode;
  final double interactionRadius;
  final bool showTrails;
  final bool showConnections;

  FluidPainter(
    this.particles,
    this.touchPoint,
    this.isTouching,
    this.isAttractMode,
    this.interactionRadius,
    this.showTrails,
    this.showConnections,
  );

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制力场范围
    if (isTouching) {
      // 计算相对位置用于渐变
      final relativeX = touchPoint.dx / size.width;
      final relativeY = touchPoint.dy / size.height;

      final gradient = RadialGradient(
        center: Alignment(relativeX * 2 - 1, relativeY * 2 - 1),
        radius: interactionRadius / size.width,
        colors: [
          (isAttractMode ? Colors.blue : Colors.red).withValues(alpha: 0.3),
          (isAttractMode ? Colors.blue : Colors.red).withValues(alpha: 0.0),
        ],
      );
      final fieldPaint = Paint()
        ..shader = gradient.createShader(
          Rect.fromCircle(center: touchPoint, radius: interactionRadius),
        );
      canvas.drawCircle(touchPoint, interactionRadius, fieldPaint);

      // 绘制边界圆环
      final ringPaint = Paint()
        ..color =
            (isAttractMode ? Colors.blue : Colors.red).withValues(alpha: 0.5)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2;
      canvas.drawCircle(touchPoint, interactionRadius, ringPaint);
    }

    // 绘制粒子连接线
    if (showConnections) {
      final connectionPaint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 0.5;
      for (int i = 0; i < particles.length; i++) {
        for (int j = i + 1; j < particles.length; j++) {
          final dx = particles[i].x - particles[j].x;
          final dy = particles[i].y - particles[j].y;
          final distance = sqrt(dx * dx + dy * dy);
          if (distance < 50) {
            final alpha = 1 - (distance / 50);
            connectionPaint.color = Colors.white.withValues(alpha: alpha * 0.2);
            canvas.drawLine(
              Offset(particles[i].x, particles[i].y),
              Offset(particles[j].x, particles[j].y),
              connectionPaint,
            );
          }
        }
      }
    }

    // 绘制粒子
    for (var particle in particles) {
      // 根据速度计算发光效果
      final speed = particle.getSpeed();
      final speedFactor = (speed / 10).clamp(0, 1);

      // 混合颜色
      final color = Color.lerp(
        particle.baseColor,
        Colors.white,
        speedFactor * 0.7,
      )!;

      final particlePaint = Paint()
        ..color = color.withValues(alpha: showTrails ? 0.3 : 0.8)
        ..style = PaintingStyle.fill;

      // 绘制光晕效果
      if (speedFactor > 0.3) {
        final glowPaint = Paint()
          ..color = color.withValues(alpha: speedFactor * 0.3)
          ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
        canvas.drawCircle(
          Offset(particle.x, particle.y),
          particle.mass * 2,
          glowPaint,
        );
      }

      // 绘制粒子本体
      canvas.drawCircle(
        Offset(particle.x, particle.y),
        particle.mass,
        particlePaint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant FluidPainter oldDelegate) {
    return oldDelegate.particles != particles ||
        oldDelegate.touchPoint != touchPoint ||
        oldDelegate.isTouching != isTouching ||
        oldDelegate.showTrails != showTrails ||
        oldDelegate.showConnections != showConnections;
  }
}
相关推荐
红色的小鳄鱼2 小时前
Vue 监视属性 (watch) 超全解析:Vue2 Vue3
前端·javascript·css·vue.js·前端框架·html5
web小白成长日记2 小时前
Vue-实例从 createApp 到真实 DOM 的挂载全历程
前端·javascript·vue.js
晚霞的不甘2 小时前
Flutter for OpenHarmony 流体气泡模拟器:用物理引擎与粒子系统打造沉浸式交互体验
前端·flutter·ui·前端框架·交互
colicode2 小时前
发送语音通知接口技术手册:支持高并发的语音消息发送API规范
前端
爱打代码的小林2 小时前
基于 OpenCV+Dlib 的实时人脸分析系统:年龄性别检测 + 疲劳监测 + 表情识别
人工智能·opencv·计算机视觉
橙露2 小时前
前端性能优化:首屏加载速度提升的8个核心策略与实战案例
前端·性能优化
Access开发易登软件2 小时前
Access 中实现 Web 风格的顶部加载进度条
前端·数据库·vba·access·access开发
孞㐑¥2 小时前
算法—字符串
开发语言·c++·经验分享·笔记·算法
一起养小猫2 小时前
Flutter for OpenHarmony 实战:打造功能完整的记账助手应用
android·前端·flutter·游戏·harmonyos