Flutter for Harmony 跨平台开发实战:双曲几何与庞加莱圆盘——非欧空间的视觉映射

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🌀 一、双曲几何:数学之美

📚 1.1 非欧几何的诞生

双曲几何是非欧几何的重要分支,由高斯、波尔约和罗巴切夫斯基在19世纪初独立发现。它打破了欧几里得几何中"过直线外一点有且只有一条平行线"的公理。

平行公理的演变

复制代码
欧几里得几何(第五公设):
过直线外一点,有且只有一条直线与已知直线平行

双曲几何(罗巴切夫斯基):
过直线外一点,至少有两条直线与已知直线平行

椭圆几何(黎曼):
过直线外一点,没有直线与已知直线平行

三种几何的比较:
┌─────────────┬─────────────┬─────────────┐
│  欧氏几何   │  双曲几何   │  椭圆几何   │
├─────────────┼─────────────┼─────────────┤
│ 平行线:1条 │ 平行线:∞条 │ 平行线:0条 │
│ 曲率:0     │ 曲率:< 0   │ 曲率:> 0   │
│ 三角形内角和│ 三角形内角和│ 三角形内角和│
│ = 180°     │ < 180°     │ > 180°     │
└─────────────┴─────────────┴─────────────┘

历史背景

复制代码
公元前300年:欧几里得《几何原本》
1829年:罗巴切夫斯基发表双曲几何
1832年:波尔约独立发现双曲几何
1854年:黎曼提出黎曼几何
1868年:贝尔特拉米给出双曲几何模型
1882年:庞加莱提出圆盘模型

📐 1.2 庞加莱圆盘模型

庞加莱圆盘模型是双曲几何的一种直观表示方法,将无限的双曲平面映射到有限的单位圆盘内。

模型定义

复制代码
庞加莱圆盘:
D = {z ∈ ℂ : |z| < 1}

点:单位圆盘内的复数或点
直线:与边界圆正交的圆弧或直径
距离:双曲距离公式

双曲距离公式:
对于两点 z₁, z₂ ∈ D

d(z₁, z₂) = arccosh(1 + 2·|z₁ - z₂|² / ((1 - |z₁|²)(1 - |z₂|²)))

简化形式:
d(0, z) = arctanh(|z|) = ½ ln((1 + |z|) / (1 - |z|))

双曲直线

复制代码
庞加莱圆盘中的"直线":

1. 过圆心的直径(欧氏直线)
         │
    ─────┼─────
         │

2. 与边界正交的圆弧
    ╭───────╮
   ╱         ╲
  │     ●     │
   ╲         ╱
    ╰───────╯

特点:
- 所有双曲直线无限延伸
- 两点确定唯一双曲直线
- 双曲直线在边界上"相交于无穷远"

🔬 1.3 双曲几何的性质

三角形内角和

复制代码
双曲三角形内角和 < 180°

面积公式:
Area = π - (α + β + γ)

其中 α, β, γ 是三角形的三个内角

特点:
- 内角和越小,面积越大
- 最大面积趋近于 π
- "理想三角形":三个顶点都在边界上,内角和 = 0

理想三角形示意:
    ╭─────────╮
   ╱           ╲
  │             │
  │             │
   ╲           ╱
    ╰─────────╯
  三个顶点在边界圆上
  内角都是 0°
  面积 = π

双曲圆

复制代码
双曲圆的定义:
到定点(圆心)距离相等的点的集合

性质:
- 双曲圆也是欧氏圆
- 但圆心位置不同
- 双曲圆心比欧氏圆心更靠近原点

示意:
    ╭─────╮
   ╱   ●   ╲      ●:双曲圆心
  │    ○     │    ○:欧氏圆心
   ╲       ╱
    ╰─────╯

极限圆

复制代码
极限圆(Horocycle):
当圆心趋向边界时的极限情况

性质:
- 通过边界上一点
- 与边界圆相切
- 是双曲几何特有的曲线

示意:
    ╭─────────╮
   ╱           ╲
  │             │
   ╲    ●       ╱  ●:边界点
    ╰─────────╯    极限圆在此点与边界相切

🎯 1.4 双曲镶嵌

双曲平面可以进行无限多种正多边形镶嵌,这是双曲几何最迷人的特性之一。

正镶嵌分类

复制代码
正镶嵌 {p, q}:
p 边形,每个顶点连接 q 个多边形

欧氏镶嵌(曲率 = 0):
{3, 6}:三角形镶嵌
{4, 4}:正方形镶嵌
{6, 3}:六边形镶嵌

双曲镶嵌(曲率 < 0):
{5, 4}:五边形镶嵌
{7, 3}:七边形镶嵌
{8, 3}:八边形镶嵌
... 无限多种

判定条件:
(p - 2)(q - 2) < 4:球面镶嵌
(p - 2)(q - 2) = 4:欧氏镶嵌
(p - 2)(q - 2) > 4:双曲镶嵌

双曲镶嵌示意

复制代码
{7, 3} 镶嵌(七边形,每顶点3个):

      ╱╲
     ╱  ╲
    ╱    ╲
   ╱      ╲
  ╱        ╲
 ╱──────────╲
╱            ╲

每个七边形的内角 = 120°
七边形内角和 = 840° < 900°(欧氏七边形)

🔧 二、双曲几何的 Dart 实现

🧮 2.1 庞加莱圆盘模型

dart 复制代码
import 'dart:math';

/// 复数类
class Complex {
  final double real;
  final double imag;
  
  const Complex(this.real, this.imag);
  
  factory Complex.fromPolar(double r, double theta) =>
      Complex(r * cos(theta), r * sin(theta));
  
  double get modulus => sqrt(real * real + imag * imag);
  double get argument => atan2(imag, real);
  
  Complex get conjugate => Complex(real, -imag);
  Complex get inverse => conjugate / (modulus * modulus);
  
  Complex operator +(Complex other) => Complex(real + other.real, imag + other.imag);
  Complex operator -(Complex other) => Complex(real - other.real, imag - other.imag);
  Complex operator *(Complex other) => Complex(real * other.real - imag * other.imag, real * other.imag + imag * other.real);
  Complex operator /(Complex other) => this * other.inverse;
  
  Offset toOffset() => Offset(real, imag);
}

/// 庞加莱圆盘模型
class PoincareDisk {
  static const double radius = 1.0;
  
  static bool isInside(Complex z) => z.modulus < radius;
  
  static double distance(Complex z1, Complex z2) {
    final diff = z1 - z2;
    final numerator = diff.modulus * diff.modulus;
    final denominator = (1 - z1.modulus * z1.modulus) * (1 - z2.modulus * z2.modulus);
    
    if (denominator <= 0) return double.infinity;
    
    return acosh(1 + 2 * numerator / denominator);
  }
  
  static double distanceFromOrigin(Complex z) {
    return atanh(z.modulus);
  }
  
  static double acosh(double x) => log(x + sqrt(x * x - 1));
  static double atanh(double x) => 0.5 * log((1 + x) / (1 - x));
  
  static Complex mobiusAdd(Complex z, Complex w) {
    final numerator = z + w;
    final denominator = Complex(1, 0) + z.conjugate * w;
    return numerator / denominator;
  }
  
  static Complex mobiusMultiply(Complex z, Complex w) {
    return z * w;
  }
  
  static Complex translate(Complex z, Complex delta) {
    return mobiusAdd(delta, z);
  }
  
  static Complex rotate(Complex z, double angle) {
    return z * Complex.fromPolar(1, angle);
  }
  
  static Complex scale(Complex z, double factor) {
    final dist = distanceFromOrigin(z);
    final newDist = dist + factor;
    final newMod = tanh(newDist);
    return Complex.fromPolar(newMod, z.argument);
  }
  
  static double metricTensor(Complex z) {
    final r = z.modulus;
    return 4 / ((1 - r * r) * (1 - r * r));
  }
  
  static double getGeodesicCurvature(Complex z1, Complex z2) {
    if (z1.modulus < 0.001 && z2.modulus < 0.001) {
      return 0;
    }
    
    final mid = Complex((z1.real + z2.real) / 2, (z1.imag + z2.imag) / 2);
    final chordLength = (z1 - z2).modulus;
    
    if (mid.modulus < 0.001) {
      return 0;
    }
    
    return chordLength / (2 * mid.modulus * sqrt(1 - mid.modulus * mid.modulus));
  }
}

/// 双曲直线(测地线)
class HyperbolicLine {
  final Complex p1;
  final Complex p2;
  
  HyperbolicLine(this.p1, this.p2);
  
  bool get isDiameter {
    final cross = p1.real * p2.imag - p1.imag * p2.real;
    return cross.abs() < 1e-10;
  }
  
  List<Complex> getPoints(int count) {
    if (isDiameter) {
      return List.generate(count, (i) {
        final t = i / (count - 1);
        return Complex(p1.real + t * (p2.real - p1.real), p1.imag + t * (p2.imag - p1.imag));
      });
    }
    
    final center = _computeCircleCenter();
    final radius = (center - p1).modulus;
    
    final angle1 = (p1 - center).argument;
    final angle2 = (p2 - center).argument;
    
    var deltaAngle = angle2 - angle1;
    if (deltaAngle > pi) deltaAngle -= 2 * pi;
    if (deltaAngle < -pi) deltaAngle += 2 * pi;
    
    return List.generate(count, (i) {
      final t = i / (count - 1);
      final angle = angle1 + t * deltaAngle;
      return center + Complex.fromPolar(radius, angle);
    });
  }
  
  Complex _computeCircleCenter() {
    final mid = Complex((p1.real + p2.real) / 2, (p1.imag + p2.imag) / 2);
    final dx = p2.real - p1.real;
    final dy = p2.imag - p1.imag;
    
    final perpX = -dy;
    final perpY = dx;
    
    final t = (mid.real * mid.real + mid.imag * mid.imag - 1) / (2 * (mid.real * perpX + mid.imag * perpY));
    
    return Complex(mid.real + t * perpX, mid.imag + t * perpY);
  }
  
  double get length => PoincareDisk.distance(p1, p2);
}

⚡ 2.2 双曲镶嵌生成

dart 复制代码
/// 双曲镶嵌生成器
class HyperbolicTiling {
  final int p;
  final int q;
  final int layers;
  
  HyperbolicTiling({required this.p, required this.q, this.layers = 3});
  
  bool get isHyperbolic => (p - 2) * (q - 2) > 4;
  
  double get interiorAngle => 2 * pi / q;
  
  double get centerToVertex => acosh(cos(pi / p) / sin(pi / q));
  
  List<List<Complex>> generatePolygons() {
    final polygons = <List<Complex>>[];
    final visited = <String>{};
    
    _generateLayer(Complex(0, 0), 0, polygons, visited);
    
    return polygons;
  }
  
  void _generateLayer(
    Complex center,
    int currentLayer,
    List<List<Complex>> polygons,
    Set<String> visited,
  ) {
    if (currentLayer > layers) return;
    
    final key = '${center.real.toStringAsFixed(6)}_${center.imag.toStringAsFixed(6)}';
    if (visited.contains(key)) return;
    visited.add(key);
    
    final polygon = _generatePolygon(center);
    polygons.add(polygon);
    
    if (currentLayer < layers) {
      for (int i = 0; i < p; i++) {
        final nextCenter = _computeNeighborCenter(center, i);
        if (nextCenter.modulus < 0.99) {
          _generateLayer(nextCenter, currentLayer + 1, polygons, visited);
        }
      }
    }
  }
  
  List<Complex> _generatePolygon(Complex center) {
    final vertices = <Complex>[];
    final r = tanh(centerToVertex / 2);
    
    for (int i = 0; i < p; i++) {
      final angle = 2 * pi * i / p;
      final localVertex = Complex.fromPolar(r, angle);
      vertices.add(PoincareDisk.mobiusAdd(center, localVertex));
    }
    
    return vertices;
  }
  
  Complex _computeNeighborCenter(Complex center, int edgeIndex) {
    final r = tanh(centerToVertex);
    final angle = 2 * pi * edgeIndex / p + pi / p;
    final direction = Complex.fromPolar(r, angle);
    return PoincareDisk.mobiusAdd(center, direction);
  }
}

/// 双曲三角形
class HyperbolicTriangle {
  final Complex a;
  final Complex b;
  final Complex c;
  
  HyperbolicTriangle(this.a, this.b, this.c);
  
  double get angleA => _computeAngle(b, a, c);
  double get angleB => _computeAngle(a, b, c);
  double get angleC => _computeAngle(a, c, b);
  
  double get angleSum => angleA + angleB + angleC;
  
  double get area => pi - angleSum;
  
  double get defect => pi - angleSum;
  
  double _computeAngle(Complex p1, Complex vertex, Complex p2) {
    final v1 = Complex(p1.real - vertex.real, p1.imag - vertex.imag);
    final v2 = Complex(p2.real - vertex.real, p2.imag - vertex.imag);
    
    final dot = v1.real * v2.real + v1.imag * v2.imag;
    final cross = v1.real * v2.imag - v1.imag * v2.real;
    
    return atan2(cross.abs(), dot);
  }
  
  List<Complex> getVertices() => [a, b, c];
  
  List<HyperbolicLine> getEdges() => [
    HyperbolicLine(a, b),
    HyperbolicLine(b, c),
    HyperbolicLine(c, a),
  ];
}

🎨 2.3 可视化组件

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

/// 双曲几何可视化绘制器
class HyperbolicPainter extends CustomPainter {
  final List<List<Complex>> polygons;
  final double scale;
  final Offset center;
  final ColorScheme colorScheme;
  
  HyperbolicPainter({
    required this.polygons,
    required this.scale,
    required this.center,
    required this.colorScheme,
  });
  
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      Paint()..color = const Color(0xFF0a0a15),
    );
    
    _drawBoundaryCircle(canvas, size);
    
    for (int i = 0; i < polygons.length; i++) {
      _drawPolygon(canvas, polygons[i], i);
    }
  }
  
  void _drawBoundaryCircle(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.white.withOpacity(0.3)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
    
    canvas.drawCircle(center, scale, paint);
  }
  
  void _drawPolygon(Canvas canvas, List<Complex> vertices, int index) {
    if (vertices.isEmpty) return;
    
    final path = Path();
    final points = <Offset>[];
    
    for (int i = 0; i < vertices.length; i++) {
      final v = vertices[i];
      final nextV = vertices[(i + 1) % vertices.length];
      
      final line = HyperbolicLine(v, nextV);
      final linePoints = line.getPoints(20);
      
      for (final p in linePoints) {
        points.add(Offset(center.dx + p.real * scale, center.dy + p.imag * scale));
      }
    }
    
    if (points.isNotEmpty) {
      path.moveTo(points[0].dx, points[0].dy);
      for (int i = 1; i < points.length; i++) {
        path.lineTo(points[i].dx, points[i].dy);
      }
      path.close();
    }
    
    final hue = (index * 137.5) % 360;
    final fillPaint = Paint()
      ..color = HSVColor.fromAHSV(0.3, hue, 0.7, 0.9).toColor()
      ..style = PaintingStyle.fill;
    
    final strokePaint = Paint()
      ..color = HSVColor.fromAHSV(1, hue, 0.8, 1).toColor()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
    
    canvas.drawPath(path, fillPaint);
    canvas.drawPath(path, strokePaint);
  }
  
  @override
  bool shouldRepaint(covariant HyperbolicPainter old) =>
      polygons.length != old.polygons.length;
}

📦 三、完整示例代码

以下是完整的双曲几何可视化示例代码:

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '双曲几何',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, brightness: Brightness.dark),
        useMaterial3: true,
      ),
      home: const HyperbolicHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('🌀 双曲几何'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildCard(context, title: '庞加莱圆盘', description: '双曲平面的圆盘模型', icon: Icons.circle_outlined, color: Colors.deepPurple,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PoincareDiskDemo()))),
          _buildCard(context, title: '双曲镶嵌', description: '正多边形镶嵌图案', icon: Icons.grid_on, color: Colors.indigo,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TilingDemo()))),
          _buildCard(context, title: '测地线', description: '双曲直线与距离', icon: Icons.show_chart, color: Colors.purple,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const GeodesicDemo()))),
          _buildCard(context, title: '双曲三角形', description: '内角和与面积', icon: Icons.change_history, color: Colors.pink,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TriangleDemo()))),
        ],
      ),
    );
  }

  Widget _buildCard(BuildContext context, {required String title, required String description, required IconData icon, required Color color, required VoidCallback onTap}) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(16),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(children: [
            Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
                child: Icon(icon, color: color, size: 28)),
            const SizedBox(width: 16),
            Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
              const SizedBox(height: 4),
              Text(description, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
            ])),
            Icon(Icons.chevron_right, color: Colors.grey[400]),
          ]),
        ),
      ),
    );
  }
}

/// 复数
class C {
  final double r, i;
  const C(this.r, this.i);
  factory C.polar(double mod, double arg) => C(mod * cos(arg), mod * sin(arg));
  double get mod => sqrt(r * r + i * i);
  double get arg => atan2(i, r);
  C get conj => C(r, -i);
  C operator +(C o) => C(r + o.r, i + o.i);
  C operator -(C o) => C(r - o.r, i - o.i);
  C operator *(C o) => C(r * o.r - i * o.i, r * o.i + i * o.r);
  C operator /(C o) => this * C(o.r / (o.r*o.r + o.i*o.i), -o.i / (o.r*o.r + o.i*o.i));
  Offset toOff(double s, Offset c) => Offset(c.dx + r * s, c.dy + i * s);
}

/// 庞加莱圆盘
double hDist(C z1, C z2) {
  final d = (z1 - z2).mod;
  final den = (1 - z1.mod * z1.mod) * (1 - z2.mod * z2.mod);
  if (den <= 0) return double.infinity;
  return acosh(1 + 2 * d * d / den);
}

double acosh(double x) => log(x + sqrt(x * x - 1));
double tanh(double x) => (exp(x) - exp(-x)) / (exp(x) + exp(-x));
double atanh(double x) => 0.5 * log((1 + x) / (1 - x));

C mobiusAdd(C z, C w) => (z + w) / (C(1, 0) + z.conj * w);

/// 庞加莱圆盘演示
class PoincareDiskDemo extends StatefulWidget {
  const PoincareDiskDemo({super.key});
  @override
  State<PoincareDiskDemo> createState() => _PoincareDiskDemoState();
}

class _PoincareDiskDemoState extends State<PoincareDiskDemo> with SingleTickerProviderStateMixin {
  late AnimationController _ctrl;
  double _time = 0;
  final List<C> _points = [];
  double _rotation = 0;

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 12; i++) {
      _points.add(C.polar(0.5, 2 * pi * i / 12));
    }
    _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _ctrl.addListener(_update);
  }
  
  void _update() { _time += 0.016; _rotation += 0.005; setState(() {}); }

  @override
  void dispose() { _ctrl.removeListener(_update); _ctrl.dispose(); super.dispose(); }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('庞加莱圆盘')),
      body: CustomPaint(painter: DiskPainter(_points, _rotation, _time), size: Size.infinite),
    );
  }
}

class DiskPainter extends CustomPainter {
  final List<C> pts;
  final double rot, t;
  DiskPainter(this.pts, this.rot, this.t);

  @override
  void paint(Canvas canvas, Size size) {
    final c = Offset(size.width / 2, size.height / 2);
    final s = min(size.width, size.height) * 0.45;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    // 边界圆
    canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.2)..style = PaintingStyle.stroke..strokeWidth = 2);
    
    // 同心圆(双曲等距线)
    for (int i = 1; i <= 5; i++) {
      final r = tanh(i * 0.3);
      canvas.drawCircle(c, r * s, Paint()..color = Colors.white.withOpacity(0.1)..style = PaintingStyle.stroke);
    }
    
    // 径向线(测地线)
    for (int i = 0; i < 12; i++) {
      final angle = rot + 2 * pi * i / 12;
      canvas.drawLine(c, Offset(c.dx + s * cos(angle), c.dy + s * sin(angle)), Paint()..color = Colors.white.withOpacity(0.1));
    }
    
    // 点和连线
    for (int i = 0; i < pts.length; i++) {
      final p = pts[i];
      final rotatedP = C.polar(p.mod, p.arg + rot);
      final off = rotatedP.toOff(s, c);
      
      canvas.drawCircle(off, 6, Paint()..color = HSVColor.fromAHSV(1, i * 30, 0.8, 1).toColor());
      
      final next = pts[(i + 1) % pts.length];
      final rotatedNext = C.polar(next.mod, next.arg + rot);
      _drawGeodesic(canvas, c, s, rotatedP, rotatedNext, HSVColor.fromAHSV(0.5, i * 30, 0.8, 1).toColor());
    }
    
    // 距离标注
    final d = hDist(pts[0], pts[1]);
    final tp = TextPainter(text: TextSpan(text: '双曲距离: ${d.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70, fontSize: 12)), textDirection: TextDirection.ltr)..layout();
    tp.paint(canvas, const Offset(10, 10));
  }
  
  void _drawGeodesic(Canvas canvas, Offset c, double s, C p1, C p2, Color color) {
    final off1 = p1.toOff(s, c);
    final off2 = p2.toOff(s, c);
    
    final cross = p1.r * p2.i - p1.i * p2.r;
    if (cross.abs() < 0.01) {
      canvas.drawLine(off1, off2, Paint()..color = color..strokeWidth = 2);
      return;
    }
    
    final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
    final dx = p2.r - p1.r, dy = p2.i - p1.i;
    final px = -dy, py = dx;
    final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
    final center = C(mid.r + t * px, mid.i + t * py);
    final radius = (center - p1).mod;
    
    final path = Path();
    final angle1 = (p1 - center).arg;
    var angle2 = (p2 - center).arg;
    var delta = angle2 - angle1;
    if (delta > pi) delta -= 2 * pi;
    if (delta < -pi) delta += 2 * pi;
    
    path.arcTo(Rect.fromCircle(center: center.toOff(s, c), radius: radius * s), angle1, delta, false);
    canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.stroke..strokeWidth = 2);
  }

  @override
  bool shouldRepaint(covariant DiskPainter old) => true;
}

/// 双曲镶嵌演示
class TilingDemo extends StatefulWidget {
  const TilingDemo({super.key});
  @override
  State<TilingDemo> createState() => _TilingDemoState();
}

class _TilingDemoState extends State<TilingDemo> {
  int _p = 7, _q = 3, _layers = 3;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('双曲镶嵌')),
      body: Column(children: [
        Expanded(child: CustomPaint(painter: TilingPainter(_p, _q, _layers), size: Size.infinite)),
        _buildControls(),
      ]),
    );
  }

  Widget _buildControls() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(color: Colors.grey[900], borderRadius: const BorderRadius.vertical(top: Radius.circular(20))),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        Row(children: [
          const Text('p (边数): ', style: TextStyle(color: Colors.white70)),
          Expanded(child: Slider(value: _p.toDouble(), min: 3, max: 12, divisions: 9, onChanged: (v) => setState(() => _p = v.toInt()))),
          Text('$_p', style: const TextStyle(color: Colors.indigo)),
        ]),
        Row(children: [
          const Text('q (连接数): ', style: TextStyle(color: Colors.white70)),
          Expanded(child: Slider(value: _q.toDouble(), min: 3, max: 8, divisions: 5, onChanged: (v) => setState(() => _q = v.toInt()))),
          Text('$_q', style: const TextStyle(color: Colors.indigo)),
        ]),
        Row(children: [
          const Text('层数: ', style: TextStyle(color: Colors.white70)),
          Expanded(child: Slider(value: _layers.toDouble(), min: 1, max: 5, divisions: 4, onChanged: (v) => setState(() => _layers = v.toInt()))),
          Text('$_layers', style: const TextStyle(color: Colors.indigo)),
        ]),
        Text(_getTypeText(), style: TextStyle(color: _getTypeColor())),
      ]),
    );
  }
  
  String _getTypeText() {
    final val = (_p - 2) * (_q - 2);
    if (val < 4) return '球面镶嵌';
    if (val == 4) return '欧氏镶嵌';
    return '双曲镶嵌 {$_p, $_q}';
  }
  
  Color _getTypeColor() {
    final val = (_p - 2) * (_q - 2);
    if (val < 4) return Colors.blue;
    if (val == 4) return Colors.green;
    return Colors.purple;
  }
}

class TilingPainter extends CustomPainter {
  final int p, q, layers;
  TilingPainter(this.p, this.q, this.layers);

  @override
  void paint(Canvas canvas, Size size) {
    final c = Offset(size.width / 2, size.height / 2);
    final s = min(size.width, size.height) * 0.45;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.2)..style = PaintingStyle.stroke..strokeWidth = 2);
    
    final polygons = _generateTiling();
    
    for (int i = 0; i < polygons.length; i++) {
      _drawPolygon(canvas, c, s, polygons[i], i);
    }
  }
  
  List<List<C>> _generateTiling() {
    final polys = <List<C>>[];
    final visited = <String>{};
    _generateLayer(C(0, 0), 0, polys, visited);
    return polys;
  }
  
  void _generateLayer(C center, int layer, List<List<C>> polys, Set<String> visited) {
    if (layer > layers) return;
    
    final key = '${center.r.toStringAsFixed(4)}_${center.i.toStringAsFixed(4)}';
    if (visited.contains(key)) return;
    visited.add(key);
    
    final r = tanh(_centerToVertex() / 2);
    final verts = <C>[];
    for (int i = 0; i < p; i++) {
      final local = C.polar(r, 2 * pi * i / p);
      verts.add(mobiusAdd(center, local));
    }
    polys.add(verts);
    
    if (layer < layers) {
      for (int i = 0; i < p; i++) {
        final nr = tanh(_centerToVertex());
        final angle = 2 * pi * i / p + pi / p;
        final next = mobiusAdd(center, C.polar(nr, angle));
        if (next.mod < 0.95) {
          _generateLayer(next, layer + 1, polys, visited);
        }
      }
    }
  }
  
  double _centerToVertex() => acosh(cos(pi / p) / sin(pi / q));
  
  void _drawPolygon(Canvas canvas, Offset c, double s, List<C> verts, int idx) {
    final path = Path();
    final points = <Offset>[];
    
    for (int i = 0; i < verts.length; i++) {
      final v = verts[i];
      final nv = verts[(i + 1) % verts.length];
      final linePts = _getGeodesicPoints(v, nv, 15);
      for (final p in linePts) {
        points.add(p.toOff(s, c));
      }
    }
    
    if (points.isNotEmpty) {
      path.moveTo(points[0].dx, points[0].dy);
      for (int i = 1; i < points.length; i++) {
        path.lineTo(points[i].dx, points[i].dy);
      }
      path.close();
    }
    
    final hue = (idx * 137.5) % 360;
    canvas.drawPath(path, Paint()..color = HSVColor.fromAHSV(0.25, hue, 0.7, 0.9).toColor()..style = PaintingStyle.fill);
    canvas.drawPath(path, Paint()..color = HSVColor.fromAHSV(1, hue, 0.8, 1).toColor()..style = PaintingStyle.stroke..strokeWidth = 1);
  }
  
  List<C> _getGeodesicPoints(C p1, C p2, int count) {
    final cross = p1.r * p2.i - p1.i * p2.r;
    if (cross.abs() < 0.01) {
      return List.generate(count, (i) {
        final t = i / (count - 1);
        return C(p1.r + t * (p2.r - p1.r), p1.i + t * (p2.i - p1.i));
      });
    }
    
    final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
    final dx = p2.r - p1.r, dy = p2.i - p1.i;
    final px = -dy, py = dx;
    final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
    final center = C(mid.r + t * px, mid.i + t * py);
    final radius = (center - p1).mod;
    
    final angle1 = (p1 - center).arg;
    var angle2 = (p2 - center).arg;
    var delta = angle2 - angle1;
    if (delta > pi) delta -= 2 * pi;
    if (delta < -pi) delta += 2 * pi;
    
    return List.generate(count, (i) {
      final angle = angle1 + delta * i / (count - 1);
      return center + C.polar(radius, angle);
    });
  }

  @override
  bool shouldRepaint(covariant TilingPainter old) => p != old.p || q != old.q || layers != old.layers;
}

/// 测地线演示
class GeodesicDemo extends StatefulWidget {
  const GeodesicDemo({super.key});
  @override
  State<GeodesicDemo> createState() => _GeodesicDemoState();
}

class _GeodesicDemoState extends State<GeodesicDemo> {
  final List<C> _pts = [C(0.3, 0.2), C(-0.2, 0.4), C(0.1, -0.3)];
  int _selectedIdx = -1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('测地线')),
      body: GestureDetector(
        onPanStart: (d) {
          final size = MediaQuery.of(context).size;
          final c = Offset(size.width / 2, size.height / 2);
          final s = min(size.width, size.height) * 0.45;
          final x = (d.localPosition.dx - c.dx) / s;
          final y = (d.localPosition.dy - c.dy) / s;
          
          for (int i = 0; i < _pts.length; i++) {
            if ((_pts[i].r - x).abs() < 0.05 && (_pts[i].i - y).abs() < 0.05) {
              setState(() => _selectedIdx = i);
              return;
            }
          }
          
          if (x * x + y * y < 0.95) {
            setState(() => _pts.add(C(x, y)));
          }
        },
        onPanUpdate: (d) {
          if (_selectedIdx >= 0) {
            final size = MediaQuery.of(context).size;
            final c = Offset(size.width / 2, size.height / 2);
            final s = min(size.width, size.height) * 0.45;
            final x = (d.localPosition.dx - c.dx) / s;
            final y = (d.localPosition.dy - c.dy) / s;
            if (x * x + y * y < 0.95) {
              setState(() => _pts[_selectedIdx] = C(x, y));
            }
          }
        },
        onPanEnd: (_) => setState(() => _selectedIdx = -1),
        child: CustomPaint(painter: GeodesicPainter(_pts), size: Size.infinite),
      ),
    );
  }
}

class GeodesicPainter extends CustomPainter {
  final List<C> pts;
  GeodesicPainter(this.pts);

  @override
  void paint(Canvas canvas, Size size) {
    final c = Offset(size.width / 2, size.height / 2);
    final s = min(size.width, size.height) * 0.45;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 2);
    
    for (int i = 0; i < pts.length; i++) {
      for (int j = i + 1; j < pts.length; j++) {
        _drawGeodesic(canvas, c, s, pts[i], pts[j], Colors.purple.withOpacity(0.5));
        
        final d = hDist(pts[i], pts[j]);
        final mid = C((pts[i].r + pts[j].r) / 2, (pts[i].i + pts[j].i) / 2);
        final tp = TextPainter(text: TextSpan(text: d.toStringAsFixed(2), style: const TextStyle(color: Colors.white54, fontSize: 10)), textDirection: TextDirection.ltr)..layout();
        tp.paint(canvas, mid.toOff(s, c) - const Offset(10, 5));
      }
    }
    
    for (int i = 0; i < pts.length; i++) {
      canvas.drawCircle(pts[i].toOff(s, c), 8, Paint()..color = Colors.white);
      canvas.drawCircle(pts[i].toOff(s, c), 5, Paint()..color = Colors.purple);
    }
  }
  
  void _drawGeodesic(Canvas canvas, Offset c, double s, C p1, C p2, Color color) {
    final cross = p1.r * p2.i - p1.i * p2.r;
    if (cross.abs() < 0.01) {
      canvas.drawLine(p1.toOff(s, c), p2.toOff(s, c), Paint()..color = color..strokeWidth = 2);
      return;
    }
    
    final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
    final dx = p2.r - p1.r, dy = p2.i - p1.i;
    final px = -dy, py = dx;
    final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
    final center = C(mid.r + t * px, mid.i + t * py);
    final radius = (center - p1).mod;
    
    final path = Path();
    final angle1 = (p1 - center).arg;
    var angle2 = (p2 - center).arg;
    var delta = angle2 - angle1;
    if (delta > pi) delta -= 2 * pi;
    if (delta < -pi) delta += 2 * pi;
    
    path.arcTo(Rect.fromCircle(center: center.toOff(s, c), radius: radius * s), angle1, delta, false);
    canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.stroke..strokeWidth = 2);
  }

  @override
  bool shouldRepaint(covariant GeodesicPainter old) => true;
}

/// 双曲三角形演示
class TriangleDemo extends StatefulWidget {
  const TriangleDemo({super.key});
  @override
  State<TriangleDemo> createState() => _TriangleDemoState();
}

class _TriangleDemoState extends State<TriangleDemo> {
  List<C> _pts = [C(0, 0), C(0.5, 0), C(0.25, 0.4)];
  int _selected = -1;

  @override
  Widget build(BuildContext context) {
    final angles = _computeAngles();
    final sum = angles.reduce((a, b) => a + b);
    final area = pi - sum;
    
    return Scaffold(
      appBar: AppBar(title: const Text('双曲三角形')),
      body: GestureDetector(
        onPanStart: (d) {
          final size = MediaQuery.of(context).size;
          final c = Offset(size.width / 2, size.height / 2);
          final s = min(size.width, size.height) * 0.4;
          final x = (d.localPosition.dx - c.dx) / s;
          final y = (d.localPosition.dy - c.dy) / s;
          
          for (int i = 0; i < _pts.length; i++) {
            if ((_pts[i].r - x).abs() < 0.08 && (_pts[i].i - y).abs() < 0.08) {
              setState(() => _selected = i);
              return;
            }
          }
        },
        onPanUpdate: (d) {
          if (_selected >= 0) {
            final size = MediaQuery.of(context).size;
            final c = Offset(size.width / 2, size.height / 2);
            final s = min(size.width, size.height) * 0.4;
            final x = (d.localPosition.dx - c.dx) / s;
            final y = (d.localPosition.dy - c.dy) / s;
            if (x * x + y * y < 0.95) {
              setState(() => _pts[_selected] = C(x, y));
            }
          }
        },
        onPanEnd: (_) => setState(() => _selected = -1),
        child: Column(children: [
          Expanded(child: CustomPaint(painter: TrianglePainter(_pts), size: Size.infinite)),
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(color: Colors.grey[900], borderRadius: const BorderRadius.vertical(top: Radius.circular(20))),
            child: Column(children: [
              Text('内角: α=${(angles[0] * 180 / pi).toStringAsFixed(1)}° β=${(angles[1] * 180 / pi).toStringAsFixed(1)}° γ=${(angles[2] * 180 / pi).toStringAsFixed(1)}°', style: const TextStyle(color: Colors.white70)),
              Text('内角和: ${(sum * 180 / pi).toStringAsFixed(1)}° < 180°', style: const TextStyle(color: Colors.pink)),
              Text('亏量: ${((pi - sum) * 180 / pi).toStringAsFixed(1)}° = 面积: ${area.toStringAsFixed(3)}', style: const TextStyle(color: Colors.pink)),
            ]),
          ),
        ]),
      ),
    );
  }
  
  List<double> _computeAngles() {
    final angles = <double>[];
    for (int i = 0; i < 3; i++) {
      final p1 = _pts[i];
      final v = _pts[(i + 1) % 3];
      final p2 = _pts[(i + 2) % 3];
      
      final v1 = C(v.r - p1.r, v.i - p1.i);
      final v2 = C(p2.r - p1.r, p2.i - p1.i);
      
      final dot = v1.r * v2.r + v1.i * v2.i;
      final cross = (v1.r * v2.i - v1.i * v2.r).abs();
      
      angles.add(atan2(cross, dot));
    }
    return angles;
  }
}

class TrianglePainter extends CustomPainter {
  final List<C> pts;
  TrianglePainter(this.pts);

  @override
  void paint(Canvas canvas, Size size) {
    final c = Offset(size.width / 2, size.height / 2);
    final s = min(size.width, size.height) * 0.4;
    
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    canvas.drawCircle(c, s, Paint()..color = Colors.white.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 2);
    
    final path = Path();
    for (int i = 0; i < 3; i++) {
      final p1 = pts[i];
      final p2 = pts[(i + 1) % 3];
      
      final cross = p1.r * p2.i - p1.i * p2.r;
      if (cross.abs() < 0.01) {
        if (i == 0) path.moveTo(p1.toOff(s, c).dx, p1.toOff(s, c).dy);
        path.lineTo(p2.toOff(s, c).dx, p2.toOff(s, c).dy);
      } else {
        final mid = C((p1.r + p2.r) / 2, (p1.i + p2.i) / 2);
        final dx = p2.r - p1.r, dy = p2.i - p1.i;
        final px = -dy, py = dx;
        final t = (mid.r * mid.r + mid.i * mid.i - 1) / (2 * (mid.r * px + mid.i * py));
        final center = C(mid.r + t * px, mid.i + t * py);
        final radius = (center - p1).mod;
        
        final angle1 = (p1 - center).arg;
        var angle2 = (p2 - center).arg;
        var delta = angle2 - angle1;
        if (delta > pi) delta -= 2 * pi;
        if (delta < -pi) delta += 2 * pi;
        
        path.arcTo(Rect.fromCircle(center: center.toOff(s, c), radius: radius * s), angle1, delta, false);
      }
    }
    path.close();
    
    canvas.drawPath(path, Paint()..color = Colors.pink.withOpacity(0.3)..style = PaintingStyle.fill);
    canvas.drawPath(path, Paint()..color = Colors.pink..style = PaintingStyle.stroke..strokeWidth = 2);
    
    for (final p in pts) {
      canvas.drawCircle(p.toOff(s, c), 8, Paint()..color = Colors.white);
    }
  }

  @override
  bool shouldRepaint(covariant TrianglePainter old) => true;
}

📝 四、数学原理深入解析

📐 4.1 双曲距离公式推导

从度量张量出发

复制代码
庞加莱圆盘的度量张量:
ds² = 4(dx² + dy²) / (1 - x² - y²)²

极坐标形式:
ds² = 4(dr² + r²dθ²) / (1 - r²)²

从原点到点 z 的距离:
d(0, z) = ∫₀^|z| 2dr / (1 - r²) = atanh(|z|)

两点间的距离:
d(z₁, z₂) = arccosh(1 + 2|z₁ - z₂|² / ((1 - |z₁|²)(1 - |z₂|²)))

推导:
利用莫比乌斯变换的不变性
将两点变换到原点和实轴上

🔄 4.2 莫比乌斯变换

莫比乌斯变换定义

复制代码
莫比乌斯变换:
f(z) = (az + b) / (cz + d)

其中 ad - bc ≠ 0

保持庞加莱圆盘的变换:
f(z) = e^(iθ) · (z - z₀) / (1 - z̄₀z)

性质:
- 保角:保持角度
- 保圆:将圆映射为圆
- 等距:保持双曲距离
- 可逆:有逆变换

变换类型

复制代码
1. 旋转:
   f(z) = e^(iθ) · z
   
2. 平移:
   f(z) = (z + a) / (1 + āz)
   
3. 缩放(沿测地线):
   f(z) = e^t · z(在特定方向)

任何等距变换都可以表示为:
f(z) = e^(iθ) · (z - z₀) / (1 - z̄₀z)

🌸 4.3 高斯-博内定理

双曲几何中的高斯-博内定理

复制代码
对于双曲三角形:

∫∫_T K dA + ∫_∂T κ_g ds = 2π

其中:
- K = -1(双曲曲率)
- κ_g = 0(测地线曲率)

简化为:
-A + (π - α - β - γ) = 0

即:
Area = π - (α + β + γ)

三角形面积 = π - 内角和

推广到多边形

复制代码
对于 n 边形:

Area = (n - 2)π - 内角和

对于正 n 边形(内角 = α):
Area = (n - 2)π - nα

当 α → 0 时(理想多边形):
Area → (n - 2)π

🎯 4.4 双曲函数

双曲函数定义

复制代码
sinh(x) = (e^x - e^(-x)) / 2
cosh(x) = (e^x + e^(-x)) / 2
tanh(x) = sinh(x) / cosh(x)

反函数:
arsinh(x) = ln(x + √(x² + 1))
arcosh(x) = ln(x + √(x² - 1))
artanh(x) = ½ ln((1 + x) / (1 - x))

恒等式:
cosh²(x) - sinh²(x) = 1

在双曲几何中的应用

复制代码
双曲距离与 tanh 的关系:
r = tanh(d/2)

其中 r 是欧氏半径,d 是双曲距离

圆周长:
C = 2π sinh(r)

圆面积:
A = 2π (cosh(r) - 1) = 4π sinh²(r/2)

🔬 五、高级应用场景

🎨 5.1 艺术与设计

埃舍尔的双曲木刻

dart 复制代码
class EscherHyperbolic {
  static List<List<C>> generatePattern(int p, int q, int layers) {
    final tiling = HyperbolicTiling(p: p, q: q, layers: layers);
    return tiling.generatePolygons();
  }
  
  static void applyFishPattern(Canvas canvas, List<C> polygon, int index) {
    // 在多边形内绘制鱼形图案
  }
}

🌐 5.2 数据可视化

双曲树布局

dart 复制代码
class HyperbolicTreeLayout {
  final TreeNode root;
  
  HyperbolicTreeLayout(this.root);
  
  Map<TreeNode, C> computeLayout() {
    final positions = <TreeNode, C>{};
    _layoutSubtree(root, C(0, 0), 0, 2 * pi, positions);
    return positions;
  }
  
  void _layoutSubtree(TreeNode node, C center, double startAngle, double endAngle, Map<TreeNode, C> positions) {
    positions[node] = center;
    
    final children = node.children;
    if (children.isEmpty) return;
    
    final angleStep = (endAngle - startAngle) / children.length;
    final childDist = 0.3;
    
    for (int i = 0; i < children.length; i++) {
      final angle = startAngle + angleStep * (i + 0.5);
      final childCenter = mobiusAdd(center, C.polar(childDist, angle));
      _layoutSubtree(children[i], childCenter, angle - angleStep / 2, angle + angleStep / 2, positions);
    }
  }
}

📱 5.3 鸿蒙多端适配

性能优化配置

dart 复制代码
class HyperbolicConfig {
  static int getMaxLayers(BuildContext context) {
    final performance = DevicePerformance.getLevel();
    switch (performance) {
      case PerformanceLevel.high: return 5;
      case PerformanceLevel.medium: return 3;
      case PerformanceLevel.low: return 2;
    }
  }
}

📊 六、性能优化策略

⚡ 6.1 镶嵌缓存

预计算优化

dart 复制代码
class TilingCache {
  static final Map<String, List<List<C>>> _cache = {};
  
  static List<List<C>> getTiling(int p, int q, int layers) {
    final key = '${p}_$q\_$layers';
    return _cache.putIfAbsent(key, () {
      final tiling = HyperbolicTiling(p: p, q: q, layers: layers);
      return tiling.generatePolygons();
    });
  }
}

💾 6.2 层级渲染

渐进式绘制

dart 复制代码
class ProgressiveRenderer {
  int _currentLayer = 0;
  final int maxLayers;
  
  ProgressiveRenderer(this.maxLayers);
  
  List<List<C>> getNextBatch() {
    if (_currentLayer >= maxLayers) return [];
    
    _currentLayer++;
    // 返回当前层的多边形
    return [];
  }
}

🎓 七、学习资源与拓展

📚 推荐阅读

主题 资源 难度
双曲几何 《非欧几何》 ⭐⭐
庞加莱圆盘 《几何拓扑》 ⭐⭐⭐
双曲镶嵌 《埃舍尔的几何艺术》 ⭐⭐
黎曼几何 《黎曼几何引论》 ⭐⭐⭐

🔗 相关项目

  • HyperRogue:双曲几何游戏
  • D. E. Shaw 的双曲浏览器:数据可视化
  • M.C. Escher 作品集:艺术应用

📝 八、总结

本篇文章深入探讨了双曲几何与庞加莱圆盘模型的数学原理及其在 Flutter 中的可视化实现。

✅ 核心知识点回顾

知识点 说明
🌀 非欧几何 打破平行公理
庞加莱圆盘 双曲平面的有限表示
📐 双曲距离 atanh 公式
🔺 双曲三角形 内角和 < 180°
🎨 双曲镶嵌 无限多种正镶嵌

⭐ 最佳实践要点

  • ✅ 使用莫比乌斯变换进行坐标变换
  • ✅ 注意边界附近的数值精度
  • ✅ 缓存镶嵌计算结果
  • ✅ 渐进式渲染提高性能

🚀 进阶方向

  • 🔮 三维双曲空间
  • ✨ 双曲网络可视化
  • 📊 双曲机器学习
  • 🎨 生成艺术设计

💡 提示:本文代码基于 Flutter for Harmony 开发,可在鸿蒙设备上流畅运行。

相关推荐
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— parseDocxXml:正则驱动的 XML 文本提取
xml·flutter
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:三方库实战 flutter_phone_direct_caller 电话拨号详解
flutter
不爱吃糖的程序媛2 小时前
Flutter-OH 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— 文件格式路由:.doc 与 .docx 的分流策略
flutter·harmonyos
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— FlutterPlugin 与 AbilityAware 双接口实现
flutter·harmonyos
LawrenceLan3 小时前
31.Flutter 零基础入门(三十一):Stack 与 Positioned —— 悬浮、角标与覆盖布局
开发语言·前端·flutter·dart
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— openLink API 与浏览器启动策略
flutter
lili-felicity3 小时前
基础入门 Flutter for OpenHarmony:第三方库实战 cryptography_flutter 加密解密详解
flutter
lqj_本人3 小时前
Flutter三方库适配OpenHarmony【apple_product_name】构建设备信息展示页面
flutter