/bilibili_thumb_up_flutter

用Flutter写一个B站一键三连按钮动画

Primary LanguageC++

0. 效果预览

Kapture_bili_flutter

1. 知识拆解

上回说到,我用Jetpack Compose写了一个“一键三连”按钮。虽然不是同一时间,但是是同一地点,今天再次挑战用Flutter写一个一键三连,看完的朋友点个赞支持一下,干了兄弟们!

同样的,我们还是分三步走:

  1. 布局
  2. 交互
  3. 动画

1.1 布局

我们的布局相对简单,只需要用一个横向的微件包裹三个竖向的微件,而图标与文字在Column默认状态下会自动对齐,因此我们可以:

本文所示代码均为简化的伪代码,仅为讲解重点,无法直接使用

Row(children: [
  // 点赞微件
  Column(children: [
    Icon(),
    Text()
  ])
  // 投币微件
  Column(children: [
    Icon(),
    Text()
  ])
  // 收藏微件
  Column(children: [
    Icon(),
    Text()
  ])
])

1.2 点击事件

要实现点击图标后变红并且数字+1,我们可以用GestureDetector微件包裹图标,它提供了onTap()点击事件回调, 因此我们可以:

// 点赞状态,默认没点
bool _thumbed = false;
// 点赞数
int _thumbCounter = 0;

GestureDetector(
	onTap: () => {
    // 点击后状态改为true,点赞数+1,并刷新界面 (有发现这里与Compose的不同点吗? 这里状态是一个基本数据类型,数据改变后,需要手动刷新UI)
		setState(() {
    	_thumbed = true;
      _thumbCounter += 1;
    }
	}),
  child: Column(children: [
    // 点过赞图标为粉色,否则灰色
    Icon(color: _thumbed ? Color(0xfffe669b) : Color(0xff60676a)),
    Text(_thumbCounter.toString())
  ])
}

1.3 长按动画

Flutter的动画系统与Compose有较大差异,个人感觉不如compose来的方便,它主要分四步来集成:

  1. SingleTickerProviderStateMixin混入State类中,用以提供vsync对象
  2. 初始化一个动画控制器AnimationController, 并提供动画所需的合适的插值器Animatable(此处使用线性的Tween)
  3. AnimatedBuilder包裹目标微件,并将步骤2的动画设置给它
  4. 播放动画
 // ...   initState中初始化动画控制器
_controller = AnimationController(duration: const Duration(milliseconds: 1500), vsync: this);
// 动画插值器的起止范围,从0-2π
_animation = Tween<double>(begin: 0, end: 2 * math.pi).animate(_controller)
  
// ...   build()中构建UI
AnimatedBuilder(
	animation: _animation,
  builder: (BuildContext context, Widget? child) {
		return Column( children: [
                  CustomPaint(
                    painter: _ArcPainter(-_animation.value),
										child: Icon(),
                  ),
                  Text(_starCounter.toString())
                ],
           );
  }
)
  
// ... 自定义画笔绘制一键三连的圆弧
class _ArcPainter extends CustomPainter {
  // 圆弧扫过的角度,由外部传入
  double sweepAngle;
	...
  @override
  void paint(Canvas canvas, Size size) {
    // 绘制圆弧,从-π/2为圆弧起始端,结束端为sweepAngle
    canvas.drawArc(
        -math.pi / 2,
        sweepAngle
    );
  }
}

// ... 在前面的GestureDetector中增加触摸事件
GestureDetector(
	onLongPressStart: (LongPressStartDetails detail) => {
    // 长按开始时播放动画, 圆弧慢慢变长
    _controller.forward()
  }
  onLongPressUp: () => {
    // 长按结束时反向播放动画,表现为圆弧慢慢缩短
    _controller.reverse()
  }
}

2. 总结

通过以上的例子,我们可以发现:

  1. Flutter采用类的方式来组织UI代码,状态被包含在其所属的State子类中, 类似于React中的类组件
  2. 而Compose采用函数的方式来组织UI代码,状态直接属于函数内部,类似于React中的函数组件
  3. 仅就此案例来说,同样为声明式UI,Flutter代码对比Compose代码稍显啰嗦,对动画的集成也需要更多的步骤来完成
  4. 但其中的编程**是类似的:UI由各个组件相互包裹嵌套来描述,并在底层转化为DOM树,UI的变化依赖于状态,状态改变UI随之改变,并在变化时由系统自动计算其Diff以减少重复渲染的成本

3. 本文所用Flutter环境

flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.3.5, on macOS 13.0.1 22A400 darwin-arm, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2022.1.4)
[✓] VS Code (version 1.73.0)
[✓] Proxy Configuration
[✓] Connected device (2 available)
[✓] HTTP Host Availability