离开标签页暂停动画

45 阅读4分钟

公司社交项目需求,有个话题银河页面,要求所有话题以气泡的形式向上飘动,位置均匀随机分布:

image.png

这个动画不算难,实现步骤如下:
1.调用接口获取所有话题dataList。
2.定时器每次将三个话题放入showList,当showList超过一定长度后记得删除头部元素
3.根据容器宽度随机生成三个位置,每个位置距离至少两个气泡宽度,与上一行话题也要保持距离,保证不重叠
4.使用v-for将话题气泡渲染到容器中
5.添加CSS动画

上述的实现都不算太难。气泡位置是绝对定位实现的,动画就是将top从110%到-10%,每行生成三个随机left

问题来了,需求上要求用户离开本页面的时候,动画暂停。这个需求看起来挺简单的,只需要监听visibilitychange事件,document.visibilityState === "hidden"时在气泡上添加animation-play-state: paused,同时停止定时器就行了。于是第一版代码这么写的:

<Topic
  :class="['son', isPaused ? 'animation-paused' : '']"
  v-for="item in showList"
 :key="item"
  :style="{ left: item.left + 'px' }"
/>


let isPaused = ref(false);
// 监听标签页切换
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    // 暂定定时器
    isPaused.value = true;
  } else {
    isPaused.value = false;
  }
});

// CSS
  .animation-paused {
    animation-play-state: paused !important;
  }

然后失败了,这么写的结果是,定时器停止了,确实不再生成气泡了。但是已经生成的在视口内的气泡的动画并没有停止,而是继续向上升起,等切回本页面的时候所有气泡已经飘走了。

查阅资料后发现,浏览器切换页签后并不会立即执行动画相关的操作,而是在页签切换回来后才执行。这就导致页签切换后,animation-paused并没有添加到Topic标签上。 于是尝试在JS代码中直接添加style属性:

// 监听标签页切换
document.addEventListener("visibilitychange", () => {
  const els = document.querySelectorAll(".son");
  if (document.visibilityState === "hidden") {
    isPaused.value = true;
    els.forEach((el: any) => {
      el.style.animationPlayState = "paused";
    });
  } else {
    isPaused.value = false;
    els.forEach((el: any) => {
      el.style.animationPlayState = "running";
    });
  }
});

这么做依旧失败了,JS添加的属性也没有生效。
最终没有找到明确的资料表明出了什么问题,但是有资料说浏览器为了节约性能会在页签隐藏后延迟计算动画属性。很可能和这个特性有关。那么解决方法就是添加一行代码void el.offsetWidth;读取元素的位置属性,这样就会强制浏览器进行布局计算,使动画属性生效

最终代码:
html:

    <div class="home-box" :style="{ backgroundImage: `url(${starBg})` }">
      <!-- 新版话题银河排版 -->
      <Topic
        :class="['son', isPaused ? 'animation-paused' : '']"
        v-for="item in showList"
        :key="item"
        :style="{ left: item.left + 'px' }"
      />
    </div>

JS

// ---------------动画相关------------------
const dataList = ref([
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
  35, 36, 37, 38, 39, 40,
]);
const showList = ref<any[]>([]);

const lastLinePosition = ref<number[]>([]);
const thisLinePosition = ref<number[]>([]);
const getOnePosition = (lastLine: any, thisLine: any) => {
  for (let i = 0; i < 10000; i++) {
    let lastLineAvailable = true;
    let thisLineAvailable = true;

    let left = Math.floor(Math.random() * 1084);

    for (let j = 0; j < lastLine.length; j++) {
      if (left - lastLine[j] < 120 && left - lastLine[j] > -120) {
        lastLineAvailable = false;
        break;
      }
    }
    for (let j = 0; j < thisLine.length; j++) {
      if (left - thisLine[j] < 120 && left - thisLine[j] > -120) {
        thisLineAvailable = false;
        break;
      }
    }

    if (lastLineAvailable && thisLineAvailable) {
      return left;
    }
  }
};
let isPaused = ref(false);
onMounted(() => {
  let index = 0;
  const interval = setInterval(() => {
    if (isPaused.value) {
      return;
    }
    // let lineCount = getRandomInt();
    let lineCount = 3;
    // 生成lineCount个位置
    for (let i = 0; i < lineCount; i++) {
      const left = getOnePosition(lastLinePosition.value, thisLinePosition.value);
      if (left === undefined) {
        console.log(left);
      }
      showList.value.push({
        value: index + i < dataList.value.length ? dataList.value[index + i] : dataList.value[index + i - dataList.value.length],
        left: left,
      });

      thisLinePosition.value.push(left as number);
    }
    lastLinePosition.value = thisLinePosition.value;
    thisLinePosition.value = [];
    index += lineCount;
    if (index > dataList.value.length - 1) {
      index = index - dataList.value.length;
      // 清理showList中已经隐藏的数据
      showList.value = showList.value.slice(dataList.value.length - 30);
    }
  }, 600);
});
// 监听标签页切换
document.addEventListener("visibilitychange", () => {
  const els = document.querySelectorAll(".son");
  if (document.visibilityState === "hidden") {
    isPaused.value = true;
    els.forEach((el: any) => {
      el.style.animationPlayState = "paused";
      void el.offsetWidth; // 强制同步布局计算
    });
  } else {
    isPaused.value = false;
    els.forEach((el: any) => {
      el.style.animationPlayState = "running";
    });
  }
});

onMounted(() => {
  //  监听鼠标移入事件
  const fa = document.querySelector(".home-box") as HTMLElement;
  fa.addEventListener("mouseenter", () => {
    isPaused.value = true;
  });

  // 监听鼠标移出事件
  fa.addEventListener("mouseleave", () => {
    isPaused.value = false;
  });
});

CSS

  .home-box {
    position: relative;
    flex: 1;
    width: 1200px;
    min-height: 686px;
    overflow: hidden;
    background-size: cover;
    border-radius: 16px;
    .son {
      top: -10%;
      animation: move 5s linear;
      animation-play-state: running;
    }
    .animation-paused {
      animation-play-state: paused !important;
    }
  }
  
  @keyframes move {
  0% {
    top: 110%;
    width: 58px;
    height: 58px;
    font-size: 8px;
  }
  50% {
    top: 50%;
    width: 116px;
    height: 116px;
    font-size: 16px;
  }
  100% {
    top: -10%;
    width: 58px;
    height: 58px;
    font-size: 8px;
  }
}