《Flutter全栈开发实战指南:从零到高级》- 17 -核心动画

引言

动画是移动应用的灵魂,能够增强页面交互效果,比如:微信下拉刷新的旋转动画、支付宝页面切换的平滑过渡、抖音点赞动效等等这些炫酷的动画效果使你的App更加出彩。今天我们就来深入探讨Flutter中那些让人惊艳的动画效果,一步步掌握动画的核心技巧!

一、动画核心类介绍

1.1 动画的本质是什么?

在深入代码之前,我们先要理解动画的本质。简单来说,动画就是在一段时间内连续改变属性值的过程。

举个例子:

  • 一个按钮从透明变成不透明(改变opacity值)
  • 一个图标从左边移动到右边(改变position值)
  • 一个容器从小变大(改变size值)

用伪代码表示就是:

dart 复制代码
// 伪代码
开始值 = 0.0
结束值 = 1.0
持续时间 = 1.0 

// 在1秒内,当前值从0.0线性变化到1.0
当前值 = 开始值 + (结束值 - 开始值) * (已用时间 / 持续时间)

1.2 动画核心类

Flutter的动画系统建立在以下四个核心类基础之上:

dart 复制代码
// 1. AnimationController
AnimationController controller;

// 2. Animation
Animation<double> animation;

// 3. Tween
Tween<double> tween;

// 4. Curve
CurvedAnimation curvedAnimation;

如果把动画效果比作开车,别想歪了,哈哈!想象一下你将车从A点开到B点:

  • AnimationController就像是你的脚踩油门和刹车,控制车的启动、停止、加速、减速
  • Tween就像是导航,告诉你从A点(开始值)到B点(结束值)的路线
  • Animation就像是车速表,实时显示当前的速度值
  • Curve就像是道路状况,决定了你是匀速前进、先快后慢,还是有什么特殊的加速模式

1.3 核心类的职责

Animation - 值容器

dart 复制代码
Animation<double> sizeAnimation;

// 主要职责:
// - 持有当前动画值
// - 通知监听器值发生变化
// - 管理动画状态

// 添加值监听器
sizeAnimation.addListener(() {
  setState(() {
    // 当动画值变化时,重建Widget。。。
  });
});

// 添加状态监听器  
sizeAnimation.addStatusListener((status) {
  switch (status) {
    case AnimationStatus.dismissed:
      print('动画在开始状态'); 
      break;
    case AnimationStatus.forward:
      print('正向执行'); 
      break;
    case AnimationStatus.reverse:
      print('反向执行'); 
      break;
    case AnimationStatus.completed:
      print('执行完成'); 
      break;
  }
});

AnimationController

dart 复制代码
class _MyAnimationState extends State<MyAnimation> 
    with SingleTickerProviderStateMixin {
  
  late AnimationController controller;
  
  @override
  void initState() {
    super.initState();
    
    // 创建动画控制器
    controller = AnimationController(
      duration: Duration(seconds: 2),  // 持续2秒
      vsync: this,                     // 垂直同步
    );
  }
  
  @override
  void dispose() {
    controller.dispose();  // 释放
    super.dispose();
  }
}

这里有个重要概念:vsync vsync的作用是防止动画在页面不可见时继续运行,造成浪费资源。通过SingleTickerProviderStateMixin,当页面被遮挡或切换到后台时,动画会自动暂停。

二、隐式动画

2.1 什么是隐式动画?

本质就是告诉Widget最终的状态是什么,Flutter会自动帮你生成过渡动画。

  • 普通Widget:不加任何动画效果,widget会很生硬的直接过渡过去;
  • 隐式动画Widget:从旧状态到新状态间有一个平滑的过渡,用户体验更好一些;

2.2 隐式动画组件

2.2.1 AnimatedContainer

AnimatedContainer:最常用的隐式动画组件,几乎所有的容器属性动画都可以用它来完成。

dart 复制代码
class AnimatedContainerExample extends StatefulWidget {
  @override
  _AnimatedContainerExampleState createState() => _AnimatedContainerExampleState();
}

class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
  // 定义可变属性
  double _width = 100.0;
  double _height = 100.0;
  Color _color = Colors.blue;
  BorderRadius _borderRadius = BorderRadius.circular(8.0);
  
  void _toggleAnimation() {
    setState(() {
      // 改变属性值
      _width = _width == 100.0 ? 200.0 : 100.0;
      _height = _height == 100.0 ? 200.0 : 100.0;
      _color = _color == Colors.blue ? Colors.green : Colors.blue;
      _borderRadius = _borderRadius == BorderRadius.circular(8.0) 
          ? BorderRadius.circular(50.0)   // 变成圆
          : BorderRadius.circular(8.0);   // 变回圆角矩形
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedContainer(
          width: _width,
          height: _height,
          duration: Duration(seconds: 1),     // 动画时长
          curve: Curves.easeInOut,            // 动画曲线
          decoration: BoxDecoration(
            color: _color,
            borderRadius: _borderRadius,
          ),
          child: Center(
            child: Text(
              '点我',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _toggleAnimation,
          child: Text('触发动画'),
        ),
      ],
    );
  }
}

AnimatedContainer支持的动画属性:

  • 尺寸widthheight
  • 颜色color
  • 边框圆角borderRadius
  • 内边距padding
  • 外边距margin
  • 阴影boxShadow
  • 变换transform

2.2.2 AnimatedOpacity

用来处理透明度变化的组件,非常适合实现显示/隐藏的过渡效果。

dart 复制代码
class AnimatedOpacityExample extends StatefulWidget {
  @override
  _AnimatedOpacityExampleState createState() => _AnimatedOpacityExampleState();
}

class _AnimatedOpacityExampleState extends State<AnimatedOpacityExample> {
  double _opacity = 1.0;  // 1.0 = 完全显示,0.0 = 完全隐藏
  
  void _toggleVisibility() {
    setState(() {
      _opacity = _opacity == 1.0 ? 0.0 : 1.0;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedOpacity(
          opacity: _opacity,
          duration: Duration(seconds: 1),
          curve: Curves.easeInOut,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Center(
              child: Text(
                '淡入淡出',
                style: TextStyle(color: Colors.white, fontSize: 20),
              ),
            ),
          ),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _toggleVisibility,
          child: Text(_opacity == 1.0 ? '隐藏' : '显示'),
        ),
      ],
    );
  }
}

使用场景:

  • 页面元素的显示/隐藏
  • 加载状态的过渡
  • 错误信息的淡入淡出

2.2.3 其他隐式动画组件

dart 复制代码
// AnimatedPadding - 内边距动画
AnimatedPadding(
  padding: EdgeInsets.all(_paddingValue),
  duration: Duration(seconds: 1),
  child: YourWidget(),
)

// AnimatedAlign - 对齐位置动画  
AnimatedAlign(
  alignment: _alignment,
  duration: Duration(seconds: 1),
  child: YourWidget(),
)

// AnimatedPositioned - 定位动画(注意:只能在Stack中使用)
AnimatedPositioned(
  left: _left,
  top: _top,
  duration: Duration(seconds: 1),
  child: YourWidget(),
)

2.3 隐式动画的工作原理

很多人会有疑问:为什么改变属性值就能产生动画?下面我们深入探讨其背后的实现原理:

sequenceDiagram participant U as 用户交互 participant S as setState() participant W as 隐式动画Widget participant E as 动画引擎 participant R as 渲染层 U->>S: 触发状态变化 S->>W: 重建Widget树 W->>E: 检测到属性变化 E->>E: 计算动画补间值 loop 每一帧 E->>W: 更新动画值 W->>R: 重绘界面 end E->>W: 动画完成

下面来看一段代码,来辅助理解隐式动画:

dart 复制代码
// 隐式动画的内部逻辑
class AnimatedContainer extends ImplicitlyAnimatedWidget {
  @override
  void didUpdateWidget(AnimatedContainer oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    // 当属性发生变化时
    if (oldWidget.width != widget.width) {
      // 创建动画控制器
      // 创建补间动画
      // 启动动画
    }
  }
}

简单来说,当setState()被调用时:

  1. Widget树重建
  2. AnimatedContainer检测到属性值变化
  3. 自动创建动画控制器和补间动画
  4. 启动动画,在指定时间内平滑过渡到新值

三、补间动画

3.1 什么是补间动画?

"补间"这个词来源于动画制作领域,意思是在起始状态和结束状态之间过渡帧

在Flutter中,Tween就是做这个工作的:

  • 你告诉它起始值(begin)和结束值(end)
  • 它负责计算中间的所有值
dart 复制代码
// 创建一个从50到200的尺寸补间动画
Tween<double> sizeTween = Tween<double>(
  begin: 50.0,   // 开始值
  end: 200.0,    // 结束值
);

// 创建一个从红色到蓝色的颜色补间动画  
ColorTween colorTween = ColorTween(
  begin: Colors.red,
  end: Colors.blue,
);

3.2 Tween的完整使用流程

dart 复制代码
class TweenAnimationExample extends StatefulWidget {
  @override
  _TweenAnimationExampleState createState() => _TweenAnimationExampleState();
}

class _TweenAnimationExampleState extends State<TweenAnimationExample> 
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _rotationAnimation;
  
  @override
  void initState() {
    super.initState();
    
    // 1. 创建动画控制器
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    // 2. 创建补间动画
    _sizeAnimation = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(_controller);
    
    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.red,
    ).animate(_controller);
    
    _rotationAnimation = Tween<double>(
      begin: 0.0,
      end: 2 * 3.14159,  // 2π弧度 = 360度
    ).animate(_controller);
    
    // 3. 启动动画
    _controller.forward();
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: Container(
            width: _sizeAnimation.value,
            height: _sizeAnimation.value,
            color: _colorAnimation.value,
            child: Center(
              child: Text(
                '动画中...',
                style: TextStyle(color: Colors.white),
              ),
            ),
          ),
        );
      },
    );
  }
}

3.3 Tween成员

Flutter提供了多种专门的Tween类:

dart 复制代码
// 尺寸
SizeTween(
  begin: Size(50, 50),
  end: Size(200, 200),
)

// 矩形区域
RectTween(
  begin: Rect.fromLTRB(0, 0, 50, 50),
  end: Rect.fromLTRB(0, 0, 200, 200),  
)

// 整数
IntTween(
  begin: 0,
  end: 100,
)

// 步进
StepTween(
  begin: 0,
  end: 100,
)

// 可以为自定义类型创建补间动画
Tween<YourCustomType>(
  begin: CustomType(),
  end: CustomType(),
)

四、动画曲线与控制器

4.1 动画曲线 - Curves

动画曲线本质就是模拟现实生活中的自然运动规律。

dart 复制代码
// 使用不同的动画曲线
Animation<double> animation = CurvedAnimation(
  parent: controller,
  curve: Curves.easeInOut,     // 先加速再减速
  reverseCurve: Curves.easeIn, // 反向动画
);

常用的动画曲线:

dart 复制代码
// 线性
Curves.linear

// 曲线
Curves.easeIn        // 先慢后快
Curves.easeOut       // 先快后慢  
Curves.easeInOut     // 先慢,中间快,后慢

// 弹性效果
Curves.bounceOut     // 回弹效果
Curves.elasticOut    // 弹簧效果

// 回弹效果
Curves.decelerate    // 先快后慢

4.2 不同曲线效果

下面创建一个不同曲线的例子:

dart 复制代码
class CurvesDemo extends StatefulWidget {
  @override
  _CurvesDemoState createState() => _CurvesDemoState();
}

class _CurvesDemoState extends State<CurvesDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Curve> _curves = [
    Curves.linear,
    Curves.easeIn,
    Curves.easeOut,
    Curves.easeInOut,
    Curves.bounceOut,
    Curves.elasticOut,
  ];
  final List<String> _curveNames = [
    'linear',
    'easeIn', 
    'easeOut',
    'easeInOut',
    'bounceOut',
    'elasticOut',
  ];
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);  // 循环
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _curves.length,
      itemBuilder: (context, index) {
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Column(
            children: [
              Text(_curveNames[index]),
              SizedBox(height: 10),
              Container(
                height: 50,
                child: AnimatedBuilder(
                  animation: _controller,
                  builder: (context, child) {
                    final animation = CurvedAnimation(
                      parent: _controller,
                      curve: _curves[index],
                    );
                    return Container(
                      width: 50 + animation.value * 200,  // 宽度从50到250
                      color: Colors.blue,
                      child: Center(
                        child: Text('${(animation.value * 100).round()}%'),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

五、AnimationController

5.1 基本操作

AnimationController:控制动画的整个生命周期。

dart 复制代码
class AnimationControllerExample extends StatefulWidget {
  @override
  _AnimationControllerExampleState createState() => _AnimationControllerExampleState();
}

class _AnimationControllerExampleState extends State<AnimationControllerExample> 
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _animation;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    _animation = Tween<double>(begin: 0, end: 300).animate(_controller);
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          height: _animation.value,
          width: _animation.value,
          color: Colors.blue,
        ),
        SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
              onPressed: _controller.forward,  // 正向
              child: Text('播放'),
            ),
            ElevatedButton(
              onPressed: _controller.reverse,  // 反向
              child: Text('倒放'),
            ),
            ElevatedButton(
              onPressed: _controller.stop,     // 暂停
              child: Text('暂停'),
            ),
            ElevatedButton(
              onPressed: () {
                _controller.reset();           // 重置
                _controller.forward();
              },
              child: Text('重置并播放'),
            ),
          ],
        ),
        SizedBox(height: 20),
        Text('当前值: ${_animation.value.toStringAsFixed(1)}'),
        Text('当前状态: ${_controller.status.toString()}'),
      ],
    );
  }
}

5.2 使用技巧

dart 复制代码
// 动画控制的高级方法

// 1. 从特定值开始
_controller.animateTo(0.5);  // 从当前值动画到0.5

// 2. 相对动画
_controller.forward(from: 0.0);  // 从0.0开始正向动画

// 3. 重复动画
_controller.repeat(         // 无限重复
  min: 0.2,                 // 最小值
  max: 0.8,                 // 最大值
  reverse: true,            // 往返执行
  period: Duration(seconds: 1), // 循环周期
);

// 4. 获取动画信息
print('是否动画中: ${_controller.isAnimating}');
print('是否完成: ${_controller.isCompleted}');
print('是否停止: ${_controller.isDismissed}');

六、自定义显式动画与性能优化

6.1 自定义显式动画

有时候隐式动画不能满足我们的需求,这时候就需要自定义显式动画。

dart 复制代码
class CustomExplicitAnimation extends StatefulWidget {
  @override
  _CustomExplicitAnimationState createState() => _CustomExplicitAnimationState();
}

class _CustomExplicitAnimationState extends State<CustomExplicitAnimation> 
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<double> _opacityAnimation;
  late Animation<Color?> _colorAnimation;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      duration: Duration(seconds: 3),
      vsync: this,
    );
    
    // 创建交错动画效果
    _sizeAnimation = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.6, curve: Curves.easeInOut), // 只在前60%时间执行
    ));
    
    _opacityAnimation = Tween<double>(
      begin: 0.3,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.2, 0.8, curve: Curves.easeIn), // 从20%到80%时间执行
    ));
    
    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.purple,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.5, 1.0, curve: Curves.easeOut), // 从50%时间开始执行
    ));
    
    _controller.forward();
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _opacityAnimation.value,
          child: Container(
            width: _sizeAnimation.value,
            height: _sizeAnimation.value,
            decoration: BoxDecoration(
              color: _colorAnimation.value,
              borderRadius: BorderRadius.circular(20),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.3),
                  blurRadius: 10,
                  offset: Offset(0, 5),
                ),
              ],
            ),
            child: Center(
              child: Text(
                '自定义动画',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

二者之间有什么区别呢?隐式动画 vs 显式动画

维度 隐式动画 显式动画
控制 自动控制 完全控制
代码量 简单 相对复杂
灵活性 有限 非常高
适用场景 简单属性变化 复杂动画序列

6.2 性能优化

动画性能很重要,不好的动画效果会让用户觉得应用卡顿。这里有几个优化技巧:

6.2.1 使用AnimatedBuilder局部重建

dart 复制代码
// 不推荐:整个页面重建
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Opacity(
      opacity: _animation.value,
      child: TestWidget(), // 组件被重复重建
    ),
  );
}

// 推荐:只重建动画部分
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Opacity(
          opacity: _animation.value,
          child: child, // 使用缓存的child
        );
      },
      child: TestWidget(), // 组件只构建一次
    ),
  );
}

6.2.2 使用Transform代替布局属性

dart 复制代码
// 不推荐:改变尺寸会触发布局重新计算
Container(
  width: _animation.value,
  height: _animation.value,
)

// 推荐:Transform只影响绘制,不触发布局
Transform.scale(
  scale: _animation.value,
  child: Container(
    width: 100,  // 固定尺寸
    height: 100,
  ),
)

6.2.3 避免在动画中使用Opacity

dart 复制代码
// 不推荐:Opacity会导致整个子树重绘
Opacity(
  opacity: _animation.value,
  child: TestComplexWidget(),
)

// 推荐:使用颜色透明度
Container(
  color: Colors.blue.withOpacity(_animation.value),
  child: TestComplexWidget(),
)

// 推荐:使用FadeTransition
FadeTransition(
  opacity: _animation,
  child: TestComplexWidget(),
)

6.3 创建一个加载动画

下面用一个加载动画效果串一下上面所讲内容:

dart 复制代码
class SmoothLoadingAnimation extends StatefulWidget {
  @override
  _SmoothLoadingAnimationState createState() => _SmoothLoadingAnimationState();
}

class _SmoothLoadingAnimationState extends State<SmoothLoadingAnimation> 
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _rotationAnimation;
  late Animation<double> _scaleAnimation;
  late Animation<Color?> _colorAnimation;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      duration: Duration(milliseconds: 1500),
      vsync: this,
    )..repeat(reverse: true);
    
    _rotationAnimation = Tween<double>(
      begin: 0.0,
      end: 2 * 3.14159,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
    
    _scaleAnimation = Tween<double>(
      begin: 0.8,
      end: 1.2,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
    
    _colorAnimation = ColorTween(
      begin: Colors.blue[300],
      end: Colors.blue[700],
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: Transform.scale(
            scale: _scaleAnimation.value,
            child: Container(
              width: 60,
              height: 60,
              decoration: BoxDecoration(
                color: _colorAnimation.value,
                borderRadius: BorderRadius.circular(30),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.2),
                    blurRadius: 10,
                    offset: Offset(0, 5),
                  ),
                ],
              ),
              child: Icon(
                Icons.refresh,
                color: Colors.white,
                size: 30,
              ),
            ),
          ),
        );
      },
    );
  }
}

总结

核心知识点

  • 理解了Animation、AnimationController、Tween、Curve的作用
  • 掌握了动画状态的生命周期管理
  • 理解了隐式动画的自动管理机制
  • 学会了创建各种类型的补间动画
  • 学会了使用Curves让动画更自然
  • 学会了创建复杂的自定义动画
  • 学会了使用AnimatedBuilder优化性能

一个好的应用整体动画效果风格要一致,同时要保持流畅度,增强用户体验。通过本节学习,相信大家已经掌握了Flutter动画的核心概念和实战技巧。多动手实现各种动画效果,不断调试优化吧~~~ 有任何问题欢迎在评论区留言,看到会第一时间回复!

相关推荐
2501_915921432 小时前
抓包技术全面指南:原理、工具与应用场景
android·ios·小程序·https·uni-app·iphone·webview
TT哇2 小时前
【计算机网络】经典易错题 1.概述 2 物理层 3.数据链路层 4.网络层
android·服务器·计算机网络
小恒恒2 小时前
Flutter 3.32.1 开发环境搭建
flutter
Morgan-Chen3 小时前
iOS开发针对苹果新系统iOS26的兼容适配
ios·objective-c·xcode·ios26
Kathleen1004 小时前
iOS--Runtime
ios·objective-c·runtime·isa
Antonio9154 小时前
【Swift】UIKit:UISegmentedControl、UISlider、UIStepper、UITableView和UICollectionView
开发语言·ios·swift
在下历飞雨4 小时前
Kuikly基础之音频播放与资源管理:青蛙叫声实现
android·ios·harmonyos
1***81535 小时前
Swift在服务端开发的可能性探索
开发语言·ios·swift
勇气要爆发5 小时前
第三阶段:ExoPlayer进阶播放器
android·音视频·exoplayer