开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
前沿
上一篇《粒子相册(上)》我们实现了由粒子构成图片的效果,今天我们继续来完善后续功能,实现出一个完整的粒子相册。
效果演示
先演示下实现后的效果,由于视频转 GIF 后会出现明显的卡帧,建议大家把项目 clone 下来自行运行体验。源码
实现
一、实现相册功能
粒子相册自然是离不开相册功能,这里我们使用 carousel_slider 插件来实现图片的轮播展示。
新增 CarouselSlider
组件,设置 autoPlay
为 true 用于自动播放,轮播间隔设为 6s
CarouselSlider(
options: CarouselOptions(
aspectRatio: 2.2,
enlargeCenterPage: true,
initialPage: 2,
autoPlay: true,
autoPlayInterval: const Duration(seconds: 6),
onPageChanged: _onPageChanged,
),
items: imageSliders,
)
在 imageSliders
中定义图片的展示样式,这里用 Image
和 Text
展示图片和名称
final List<Widget> imageSliders = imgList
.map((item) => Container(
margin: const EdgeInsets.all(5.0),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
child: Stack(
children: <Widget>[
Image.asset(item, fit: BoxFit.cover, width: 1000.0),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromARGB(200, 0, 0, 0),
Color.fromARGB(0, 0, 0, 0)
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 20.0),
child: Text(
'No. ${imgList.indexOf(item)} image',
style: const TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
),
),
],
)),
)).toList();
最后在 _onPageChanged
中监听图片的切换,在每次切换轮播图片后重新加载新的粒子图片
void _onPageChanged(int index, CarouselPageChangedReason reason) {
debugPrint("index=$index, ${reason.toString()}");
particleManage.reset();
initImage(index: index);
}
优化图片的尺寸不一致问题,由于每张图片的尺寸都是不固定的,为了让展示的效果一致,这里需要对展示的图片进行裁剪缩放处理
/// 图片转换粒子
void imageToParticle(){
if(imagePic == null) return;
int width = imagePic!.width;
int height = imagePic!.height;
double aspect = width / height;
int size = min(width, height);
int left = aspect > 1 ? (width - height) ~/ 2 : 0;
int top = aspect < 1 ? (height - width) ~/ 2 : 0;
for(int i = 0; i < 200; i++) {
for(int j = 0; j < 200; j++) {
// image库获取的x、y和Flutter相反,需要把j做为x轴
int x = left + j * size ~/ 200;
int y = top + i * size ~/ 200;
...
}
}
}
实现效果:
如果仅仅只是实现如此功能,自然是没有必要重新写一篇文章,下面让我们进入今天的正式环节。
二、粒子属性配置
我们在外面的粒子图片中新增一个进入粒子详情的入口,用于粒子详情页的展示
Navigator.pushNamed(context, "/detail", arguments: imgList[pageIndex]);
前面粒子图片的展示都只是简单的展示了下效果,还不够完善,我们在详情页中增加更加细致的粒子属性设置,便于用户自定义配置。
- 粒子颗粒度
我们先在粒子管理器 PrticleManage
中新增 granularity
用于定义粒子的颗粒度
// 粒子颗粒度
int granularity = 200;
然后使用 Slider
组件来实现颗粒度属性的调节
Slider(
value: granularity,
min: 50,
max: 400,
activeColor: Colors.redAccent,
divisions: 7,
inactiveColor: Colors.green.withAlpha(99),
onChanged: (value) => _onSliderChange(value, SliderType.granularity),
onChangeEnd: (value) =>
_onSliderChangeEnd(value, SliderType.granularity),
)
最后在 _onSliderChangeEnd
回调中更新设置的粒子颗粒度
particleManage.setGranularity(value);
imageToParticle();
/// 初始化粒子
void initParticles() {
List<Particle> list = [];
double size = 400 / granularity;
for (int i = 0; i < granularity; i++) {
for (int j = 0; j < granularity; j++) {
...
}
}
setParticleList(list);
}
实现效果:
- 粒子运动速度
同理,我们新增在 ParticleManage
中新增 speed
属性,用来定义例子的移动速度
// 粒子移动速度
double speed = 5.0;
定义 Slider
的属性值设置,最大速度设为 1 最小设为 10
Slider(
value: speed,
min: 1.0,
max: 10.0,
activeColor: Colors.redAccent,
divisions: 9,
inactiveColor: Colors.green.withAlpha(99),
onChanged: (value) => _onSliderChange(value, SliderType.speed),
onChangeEnd: (value) => _onSliderChangeEnd(value, SliderType.speed),
)
在 _onSliderChangeEnd
中更新粒子速度
particleManage.setSpeed(value);
if (this.speed == speed) return;
this.speed = speed;
for (Particle particle in particleList) {
particle.ax = speed + random.nextDouble() * 10;
particle.ay = speed + random.nextDouble() * 10;
}
- 粒子离散范围
粒子离散范围的设置也是一样,定义 Slider
最小为 50 最大 300 。同样通过 _onSliderChangeEnd
在 ParticleManage
中更新粒子参数
Slider(
value: range,
min: 50.0,
max: 300.0,
activeColor: Colors.redAccent,
divisions: 5,
inactiveColor: Colors.green.withAlpha(99),
onChanged: (value) => _onSliderChange(value, SliderType.range),
onChangeEnd: (value) => _onSliderChangeEnd(value, SliderType.range),
)
particleManage.setRange(value);
if (this.range == range) return;
this.range = range;
for (Particle particle in particleList) {
particle.cx = particle.x - (random.nextDouble() * range - range ~/ 2);
particle.cy = particle.y - (random.nextDouble() * range - range ~/ 2);
}
实现效果:
三、粒子动画
上一篇文章我们介绍了打印机动画、和粒子动画,我们先把它们整合进来。在 actions
中添加 PopupMenuButton
用于多种动画的切换
PopupMenuButton(
itemBuilder: (context) {
return [
const PopupMenuItem<int>(
value: 0,
child: Text("打印动画"),
),
const PopupMenuItem<int>(
value: 1,
child: Text("粒子运动动画"),
),
];
},
onSelected: _onAnimChanged,
)
在 ParticleManage
中新增 anim
属性,并新增 Anim
枚举用于粒子动画的类型定义
// 粒子动画类型
Anim anim = Anim.particleMotion;
enum Anim {
printer,
particleMotion,
}
最后通过在 ParticleManage
的 reset
方法中重新定义粒子的当前位置和加速度来实现不同的动画效果
case Anim.printer: // 打印机动画
particle.cx = particle.x;
particle.cy = particle.y - 400;
particle.ax = speed;
particle.ay = speed + 2;
break;
case Anim.particleMotion: // 粒子运动
particle.cx = particle.x - (random.nextDouble() * range - range ~/ 2);
particle.cy = particle.y - (random.nextDouble() * range - range ~/ 2);
particle.ax = speed + random.nextDouble() * 10;
particle.ay = speed + random.nextDouble() * 10;
break;
- 原点动画
我们新增第 3 种动画原点动画,动画是以中心点为原点开始从小到大展示整张图片
粒子的 cx
、cy
和 ax
、ay
的属性设置如下:
int index = granularity ~/ 2;
particle.cx = index * particle.size - particle.size * 0.5;
particle.cy = index * particle.size - particle.size * 0.5;
particle.ax = speed;
particle.ay = speed;
- 印刷动画
前面我们的打印机动画是整张图片从上到下慢慢平移出来,其实还有另外一种更好的印刷动画效果。
在 ParticleManage
中新增 my属性
,用于动画执行进度的衡量,即 动画执行进度
= my
/ 组件高度(400)
。
// 粒子动画执行距离,总距离是400
double my = 0;
这一次不再让图片从 -400
的位置开始移动,而是直接在当前位置显示
case Anim.printer2:
particle.cx = particle.x;
particle.cy = particle.y;
particle.ax = speed;
particle.ay = speed + 2;
break;
然后在粒子的更新 updateParticle
中,新增例子颜色透明度的判断。当粒子当前的 y 坐标 cy
< my
时,粒子透明度不为空。
if (anim == Anim.printer2) {
particle.color =
particle.color.withAlpha(particle.cy <= my ? 255 : 0);
}
最后我们需要完善下粒子运动 completed
的逻辑判断,确保动画能够完全执行。
/// 粒子是否已移动到指定位置
bool isParticleCompleted(Particle particle) {
if (my > 0) {
return my >= particle.y &&
particle.cx == particle.x &&
particle.cy == particle.y;
} else {
return particle.cx == particle.x && particle.cy == particle.y;
}
}
实现效果:
- 粒子动画2
由于我们在 ParticleManage
中新增了 my属性
,用于动画执行进度的衡量,因此我们能够在原来的粒子动画中做出更细致的动画效果。
我们首先在 setParticleAnim
中新增 particleMotion2
动画的粒子属性设置
case Anim.particleMotion2: // 粒子运动2
particle.cx = particle.x - (random.nextDouble() * range - range ~/ 2);
particle.cy = particle.y + 100;
particle.ax = speed + random.nextDouble() * 10;
particle.ay = speed + random.nextDouble() * 5;
my = 50;
break;
然后在 updateParticle
中新增判断,只有在 my - 10
范围内的粒子才开始移动,同时新增粒子 alpha 判断,只有当粒子位置小于 my 时,才显示粒子
if (anim == Anim.particleMotion2) {
particle.color =
particle.color.withAlpha(particle.cy <= my ? 255 : 0);
if (particle.cy - my < 10) {
if (particle.cy > particle.y) {
particle.cy = max(particle.y, particle.cy - particle.ay);
} else if (particle.cy < particle.y) {
particle.cy = min(particle.y, particle.cy + particle.ay);
}
}
}
实现效果:
总结
今天主要实现粒子相册的相册功能,并且新增粒子颗粒度、运动速度、离散范围等具体属性配置,最后对原有的粒子动画进行整理归纳,通过新增my属性来控制粒子的动画执行进度从而做出更细致的粒子动画效果。
写下这篇文章的时候是我🐑了的第四天,虽然我症状比较轻,但这个病毒依旧让我前3天十分难受。而且身边🐑了的朋友基本都有症状,有的甚至一周还没缓过来。希望大家不要轻视这个病毒,能晚🐑尽量晚点。