阅读 116

做一个漂亮的列表展开动画

在阅读前希望读者对FLIP,浏览器动画,层叠规则有较熟悉的认知。

gif有掉帧抖动,可在下面的仓库GitPage中体验丝滑的动画

源码: github.com/moayuisuda/…

力不从心的FLIP

对于普通的缩放位移,如图片的位置改变与缩放,利用FLIP可以很容易的实现,但是对于上图中那样有内容变化的动画,光用FLIP就会显得比较力不从心。

  1. 如果只使用FLIP,将结束状态缩小,缩小后将会是一个铺满屏幕的详情页的缩小图,而不是一开始的小图,无法达到FLIP中结束状态transform后与开始状态视觉一致。

  2. 上图动画的灵魂--小图的延展与变高,也不能包含在FLIP的终态中,如同上面所说,如果作为终态,那么transform回去的时候长宽比是不一致的,无法达到视觉一致,需要将其与FLIP分开进行。

  3. 我将整个动画分为:

    (1) 初始态:初始的dom状态。

    (2) FLIP初态:FLIP终态经过transform后与初始态视觉一致的状态。

    (3) FLIP终态:FLIP取消transform后的状态。

    (4) 展开态:FLIP变为终态的同时加入额外动画(内容展开,头图延展)。

dom结构

  <div class="dialog">
    <div class="dialog_banner">
      <img src="./cover.png" alt="" />
    </div>
    <div class="dialog_content">
      <h1 class="dialog_content_title">This is the title 1</h1>
      <article>
        We’ve always defined ourselves by the ability to overcome the impossible. And we count these moments. These moments when we dare to
        aim higher, to break barriers, to reach for the stars, to make the unknown known. We count these moments as our proudest achievements.
        But we lost all that. Or perhaps we’ve just forgotten that we are still pioneers. And we’ve barely begun. And that our greatest
        accomplishments cannot be behind us, because our destiny lies above us.
      </article>
    </div>
    <h2 class="dialog_coverTitle">This is title 1</h4>
  </div>
复制代码

为了方便,这里只看会被展开的元素部分。

dialog为之后会展开的dom。

dialog_banner为小图

dialog_content为展开后异步加载的内容

dialog_coverTitle则是未展开时右下角的标题

/* index.css */

.dialog {
  position: relative;
  height: 260px;
  overflow: hidden;
  width: 100%;
  cursor: pointer;
  background-color: white;
}

.dialog_banner > img {
  width: 100%;
  height: 260px;
  object-fit: cover;
}

.dialog_coverTitle {
  margin: 0;
  transition: 0.5s;
  position: absolute;
  bottom: 10px;
  right: 10px;
  color: white;
  opacity: 1;
}
复制代码

起始态与终态

终态

.open {
  width: 100vw;
  height: 100vh;
  position: fixed;
  overflow: auto;
  left: 0;
  top: 0;
}
复制代码

可以看到展开后的dialog是一个fixed,全屏的元素。

首先来确定FLIP阶段的终态(注意FLIP终态和展开状态不一样,FLIP终态是展开状态的其中一部分),下面是FLIP相关代码,可以先不管t.x t.y t.scalet用来记录位移与scale,在退出时可复用。

  const start = item.getBoundingClientRect(); // item是dialog
  const ratio = (t.ratio = img.getBoundingClientRect().width / img.getBoundingClientRect().height); // 获取小图的长宽比
  item.classList.add("open"); // 变为FLIP终态
  item.style.zIndex = 1; // 设置zIndex保证层叠顺序
  const end = (t.end = item.getBoundingClientRect());
  const moveX = (t.x = start.x - end.x);
  const moveY = (t.y = start.y - end.y);
  const scale = (t.scale = start.width / end.width);
  const h = (img.style.height = item.style.height = item.getBoundingClientRect().width / ratio + "px"); // 扩大后的高宽比一致
复制代码

FLIP终态就是一个fixed的,占满屏幕的小图放大图。

起始态

FLIP的起始态无非就是transform后达到视觉一致的一个fixed元素。

 item.style.transformOrigin = "0 0";
 item.style.transform = `translate(${moveX}px,${moveY}px) scale(${scale})`;
复制代码

展开动画

定义一个double raf函数,来准确在下一帧触发动画。

function dbRaf(fn) {
  requestAnimationFrame(t => {
    requestAnimationFrame(t => {
      fn();
    });
  });
}
复制代码

触发

item.style.transformOrigin = "0 0";
item.style.transform = `translate(${moveX}px,${moveY}px) scale(${scale})`;

dbRaf(function() {
    img.classList.add("animate"); // 添加transition属性
    item.classList.add("animate");
    img.style.height = parseInt(h, 10) + 100 + "px"; // 图片变高效果
    item.style.height = "100vh"; // 内容展开效果
    item.style.transform = ""; // 触发动画
    item.style.overflow = "auto"; // 内容不再hidden
});

// 动画结束后移除transition
delay(function() {
    item.classList.remove("animate");
    img.classList.remove("animate");
}, 1000);
复制代码

缩小动画

因为已经得到了动画的位移与scale,所以直接使用即可。

  item.classList.add("animate--out"); // 退出动画transition设置为500ms
  img.classList.add("animate--out");
  window.addEventListener("wheel", lock, { passive: false });
  item.scrollTo({ top: 0, behavior: "smooth" });
  item.style.transform = `translate(${t.x}px,${t.y}px) scale(${t.scale})`;
  img.style.height = item.style.height = `${t.end.width / t.ratio}px`; // 变回小图的长宽比

  delay(function() {
    window.removeEventListener("wheel", lock);
    item.classList.remove("open", "animate--out");
    img.classList.remove("animate--out");
    item.style.height = img.style.height = "260px"; // 260是原小图高度,可以自己设定。
    item.style.overflow = "hidden"; // 隐藏content
    item.style.transform = "";
    item.style.zIndex = "auto";
  }, 500);
复制代码

上方有一个lock函数,在退出时锁定滚动,否则动画最后的位置会出现偏差。

最后

我并没有处理滚动穿透,网上的方案一抓一大把也没必要再写一遍了。

整个动画比较复杂的地方就在上面了,还有很多细节处理并没有写在文中,比如退出时的滚动处理,动画锁等等。