likes
comments
collection
share

粒子相册(下)

作者站长头像
站长
· 阅读数 7

前沿

效果演示

先演示下实现后的效果,由于视频转 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 中定义图片的展示样式,这里用 ImageText 展示图片和名称

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]);

前面粒子图片的展示都只是简单的展示了下效果,还不够完善,我们在详情页中增加更加细致的粒子属性设置,便于用户自定义配置。

  1. 粒子颗粒度

我们先在粒子管理器 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);
  }

实现效果:

粒子相册(下)

  1. 粒子运动速度

同理,我们新增在 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;
    }
  1. 粒子离散范围

粒子离散范围的设置也是一样,定义 Slider 最小为 50 最大 300 。同样通过 _onSliderChangeEndParticleManage 中更新粒子参数


  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,
}

最后通过在 ParticleManagereset 方法中重新定义粒子的当前位置和加速度来实现不同的动画效果

  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;
  1. 原点动画

我们新增第 3 种动画原点动画,动画是以中心点为原点开始从小到大展示整张图片

粒子的 cxcyaxay 的属性设置如下:

  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;
  1. 印刷动画

前面我们的打印机动画是整张图片从上到下慢慢平移出来,其实还有另外一种更好的印刷动画效果。

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;
    }
  }

实现效果:

粒子相册(下)

  1. 粒子动画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属性来控制粒子的动画执行进度从而做出更细致的粒子动画效果。

源码 particle_album

写下这篇文章的时候是我🐑了的第四天,虽然我症状比较轻,但这个病毒依旧让我前3天十分难受。而且身边🐑了的朋友基本都有症状,有的甚至一周还没缓过来。希望大家不要轻视这个病毒,能晚🐑尽量晚点。