>웹 프론트엔드 >CSS 튜토리얼 >트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

青灯夜游
青灯夜游앞으로
2022-02-09 19:16:053407검색

트랜지션에 대해 알고 계시나요? 혹시 Transition에 대해 모르시나요? 다음 글은 그림과 글의 조합을 통해 Transition에 대한 심층적인 이해를 돕겠습니다.

트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

이 글에서는 전환 애니메이션에 대해 자세히 알아봅니다. 맞습니다, CSS3 전환 애니메이션입니다. 뭐 이렇게 쉽게 얘기할 수 있지 않느냐고 물으실 수도 있습니다. Transition 动画。没错,CSS3 Transition 动画。你可能会问,不是很简单吗,这什么好讲的?

确实,Transition 动画使用起来非常容易。只需要给元素加上 transition-delay, transition-duration, transition-property, transition-timing-function 属性就可以有过滤效果。更简单的用法是直接使用简写的 transition 属性:

transition: <property> <duration> <timing-function> <delay>;

// transition-delay 默认为 0
// transition-property 默认为 all
// transition-timing-function 默认为 ease
transition: 0.3s;

由于 transition 动画用起来几乎没有成本,一直以来也没有太深入学习,最近翻看源代码和 MDN 文档之后发现有些知识没有理解到位,于是乎有了这篇文章,希望对读者更深入了解 Transition 动画有所帮助。(学习视频分享:css视频教程

为了尽量降低阅读理解成本,这篇文章会写得稍微啰嗦一点点,大部分示例都会配图 ——【多图预警开始!】

什么是 Transition?

简单的说就是过渡动画,通常修改 DOM 节点的样式都是立即更新在页面上的,例如修改宽高,修改透明度,修改背景色等等。

例如当鼠标移动至按钮上时,为了突出按钮的可交互,会在 hover 时修改它的样式,让用户注意到它。没有加 transition 过渡动画,给用户的感觉会很僵很生硬。

.button {
  // ...
  background-color: #00a8ff;
}

.button:hover {
  background-color: #fbc531;
  transform: scale(1.2);
}

트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

加上 transition 一行代码之后,变化就会比较顺滑。

.button {
  // ...
  transition: 1s;
}
// ...

这个例子中我们修改了 background-colortransform,结合 transition 属性,浏览器就会自动让属性值随着时间变化,从旧值逐步过渡到过渡新值,视觉上就是动画效果。

트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

区分于 AnimationTransition 动画侧重于表现一次过渡效果,从开始到结束的变化。而 Animation 不需要变化,可以循环播放 ▶️。

需要注意,并不是所有的属性变化都会有过渡效果

  1. 有些 CSS 属性只支持枚举值,非黑即白,不存在中间状态,例如 visibility: visible; 被修改成 visibility: hidden; 不会有动画效果,因为不存在可见又不可见的中间状态。在浏览器上的表现是 duration 到了之后元素立即突变为 hidden。

    .button:hover {
      //...
      visibility: hidden;
    }

    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

  2. 有些属性虽然是可计算数值,但天生注定不能有过渡效果,例如 transition-delaytransition-duration 都是立即生效,这里值得补一句由于 transition-* 属性是即时生效,这行代码如果是 hover 时才加上,那么效果会是 hover 时有动画,移出时没有动画。
  3. 即使是可过渡的属性变化,也可能因为无法计算中间状态而失去过渡效果。例如 box-shadow 属性虽然支持 transition 的动画的,但如果从 "outset" 切换到 inset,也是突变的。

    .button {
      // ...
      box-shadow: 0 0 0 1px rgb(0 0 0 / 15%);
      transition: 1s;
    }
    
    .button:hover {
      // ...
      box-shadow: inset 0 0 0 10px rgb(0 0 0 / 15%);
    }
    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!
    从表现上看,box-shadow 的变化是 hover 上去立马就生效了。
  4. 如果某个属性值是连续可计算的数值,但是变化前后变成散列的枚举值,那么过渡也不会生效。例如从 height: 100px => height: auto
  5. 실제로 전환 애니메이션은 사용하기 매우 쉽습니다. transition-delay, transition-duration, transition-property, transition-timing-function을 요소에 추가하기만 하면 됩니다. > 속성에는 필터링 효과가 있을 수 있습니다. 더 간단한 사용법은 축약된 transition 속성을 ​​직접 사용하는 것입니다:
<div class="wrapper">
    <div id="button"></div>
    <div id="popup"></div>
</div>

전환 애니메이션은 사용하기에 거의 비용 효율적이기 때문에 최근에는 소스 코드를 살펴보고 깊이 연구하지 않았습니다. MDN 문서에서 지식이 완전히 이해되지 않은 부분을 발견하여 독자들이 Transition 애니메이션에 대해 더 깊이 이해하는 데 도움이 되기를 바랍니다. (학습 영상 공유: css 영상 튜토리얼)

비용을 줄이기 위해 최대한 독해력을 키우기 위해 이번 글은 조금 장황하게 설명할 예정이며, 대부분의 예시는 사진과 함께 하겠습니다 - [복수 사진 주의 시작! ]

전환이란?

간단히 말하면 전환 애니메이션입니다. 일반적으로 DOM 노드의 스타일을 수정하면 너비와 높이 수정, 투명도 수정, 수정 등 페이지에 즉시 업데이트됩니다. 배경색 등 잠깐만요. 트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

예를 들어 마우스를 버튼 위로 이동할 때 버튼의 상호 작용을 강조하기 위해 마우스를 올리면 사용자가 알 수 있도록 스타일이 수정됩니다. 전환 애니메이션이 없으면 사용자는 뻣뻣하고 뻣뻣한 느낌을 받게 됩니다. 🎜
const btn = document.querySelector("#button");
const popup = document.querySelector("#popup");

if (!popup.classList.contains("active")) {
    popup.style.display = "block";
    popup.classList.add("active");
} else {
    popup.style.display = "none";
    popup.classList.remove("active");
}
🎜트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!🎜🎜 전환 코드 줄을 추가하면 변경이 더 원활해집니다. 🎜
#popup {
  display: none;
  opacity: 0;
  transform: translateY(-8px);
  transition: 1s;

  &.active {
    opacity: 1;
    transform: translateY(0%);
  }
}
🎜이 예에서는 background-colortransformtransition 속성과 결합하여 수정했으며, 브라우저는 속성 값을 자동으로 변경합니다. 이전 값에서 과도기적인 새 값으로 점진적으로 전환되는 시간 변화는 시각적으로 애니메이션 효과입니다. 🎜🎜트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!🎜
🎜애니메이션과 달리 전환 애니메이션은 전환 효과, 처음부터 끝까지의 변화를 표현하는 데 중점을 둡니다. 애니메이션은 변경할 필요가 없으며 ▶️ 반복 재생이 가능합니다. 🎜
🎜모든 속성 변경에 전환 효과가 있는 것은 아닙니다.🎜
  1. 🎜일부 CSS 속성은 검은색 또는 흰색인 열거형 값만 지원하며 visibility: visible;과 같은 중간 상태는 visibility: hide;로 수정되며, visible과 visible 사이에는 중간 상태가 없기 때문에 애니메이션 효과가 없습니다. 보이지 않는. 브라우저의 성능은 기간에 도달한 후 즉시 요소가 숨김으로 변경된다는 것입니다. 🎜
    btn.addEventListener("click", () => {
      if (!popup.classList.contains("active")) {
        popup.style.display = "block";
        setTimeout(() => {
          popup.classList.add("active");
        }, 0);
      } else {
        popup.classList.remove("active");
        setTimeout(() => {
          popup.style.display = "none";
        }, 600);
      }
    });
    🎜트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!
    🎜🎜
  2. 일부 속성은 계산 가능한 값이지만 전환 효과가 없습니다. 예를 들어 transition-delaytransition-duration은 모두 적용됩니다. transition-* 속성이 즉시 적용되므로 마우스를 가져가는 동안 이 코드 줄을 추가하면 마우스를 가져갈 때 효과가 애니메이션이 되고 밖으로 나갈 때 애니메이션이 발생하지 않는다는 점을 여기에 추가할 가치가 있습니다. 🎜
  3. 🎜전환 가능한 속성 변경이라도 중간 상태를 계산할 수 없기 때문에 전환 효과가 손실될 수 있습니다. 예를 들어 box-shadow 속성은 전환 애니메이션을 지원하지만 "outset"에서 inset으로 전환하면 돌연변이. 🎜
    if (!popup.classList.contains("active")) {
        popup.style.display = "block";
    
        requestAnimationFrame(() => {
            popup.classList.add("active");
        });
    }

    성능 관점에서 볼 때 상자 그림자의 변경 사항은 마우스를 올리면 즉시 적용됩니다. 🎜
  4. 속성 값이 연속적으로 계산 가능한 값이지만 변경 전후에 해시된 열거형 값으로 변경되면 전환이 적용되지 않습니다. 예를 들어 height: 100px => height: auto에서는 애니메이션이 없습니다. 🎜🎜🎜위 내용은 Transition의 기본 사용법을 검토한 것입니다. 이제 실제 개발 시나리오에서 직면하게 될 문제를 살펴보겠습니다. 🎜🎜전환 애니메이션이 적용되지 않는 이유는 무엇인가요? 🎜🎜시나리오 질문: 이제 사용자 정의 드롭다운 선택기에 대한 애니메이션 요구 사항을 받았다고 가정합니다. 디자이너가 제공한 렌더링은 다음과 같습니다. 🎜🎜🎜🎜

    这是很常见的出现-消失动画,在很多组件库里面都会出现,点击触发器(按钮)时才在页面上渲染 Popup (下拉内容),并且 Popup 出现的同时需要有渐现和下滑的动画;展开之后再次点击按钮,Popup 需要渐隐和上滑。

    平时使用的时候并没有过多注意它的实现,不妨现在让我们动手试验一下。

    暂时忽略 popup 的内容,用了个 div 来占位模拟,HTML 结构很简单。

    <div class="wrapper">
        <div id="button"></div>
        <div id="popup"></div>
    </div>

    在点击按钮的时候,让 popup 显示/隐藏,然后切换 popup.active 类名。

    const btn = document.querySelector("#button");
    const popup = document.querySelector("#popup");
    
    if (!popup.classList.contains("active")) {
        popup.style.display = "block";
        popup.classList.add("active");
    } else {
        popup.style.display = "none";
        popup.classList.remove("active");
    }

    编写 CSS 样式,在不 active 时透明度设置为 0,向上偏移,active 时则不偏移且透明度设置为 1。

    #popup {
      display: none;
      opacity: 0;
      transform: translateY(-8px);
      transition: 1s;
    
      &.active {
        opacity: 1;
        transform: translateY(0%);
      }
    }

    完整代码 在这里,看起来代码没什么问题,点击按钮切换的时候,popup 应该会有动画过渡效果。然而实际运行效果:

    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    硬邦邦地完全没有过渡效果,这是为啥?明明已经设置了 transition,且 opacitytranslateY 都是可计算可过渡的数值,也产生了变化,浏览器为什么不认呢?

    在查文档之前,我们先尝试使用万精油 setTimeout

    方案一:setTimeout 万精油

    修改 JS 代码:

    btn.addEventListener("click", () => {
      if (!popup.classList.contains("active")) {
        popup.style.display = "block";
        setTimeout(() => {
          popup.classList.add("active");
        }, 0);
      } else {
        popup.classList.remove("active");
        setTimeout(() => {
          popup.style.display = "none";
        }, 600);
      }
    });

    可以看到添加了 setTimeout 之后,transition 动画就生效了。

    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    隐藏时的 setTimeout 600ms 对应 CSS 中设置的 transition: 0.6s,就是动画完成之后才将 display 设置为 none

    主要困惑的点在于为什么显示的时候也需要加 setTimeout 呢?setTimeout 0 在这里起到的作用是什么?带着问题去翻看规范文档。

    在规范文档的 Starting of transitions 章节找到下面这段话:

    When a style change event occurs, implementations must start transitions based on the computed values that changed in that event. If an element is not in the document during that style change event or was not in the document during the previous style change event, then transitions are not started for that element in that style change event.

    翻译一下,当样式变更事件发生时,实现(浏览器)必须根据变更的属性执行过渡动画。但如果样式变更事件发生时或上一次样式变更事件期间,元素不在文档中,则不会为该元素启动过渡动画。

    结合浏览器构建 RenderTree 的过程,我们可以很清晰地定位到问题:当样式变更时间发生时,display: none 的 DOM 元素并不会出现在 RenderTree 中(style.display='block' 不是同步生效的,要在下一次渲染的时候才会更新到 Render Tree),不满足 Starting of transitions 的条件。

    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    所以 setTimeout 0 的作用是唤起一次 MacroTask,等到 EventLoop 执行回调函数时,浏览器已经完成了一次渲染,再加上 .active 类名,就有了执行过渡动画的充分条件。

    优化方案二:精准卡位 requestAnimationFrame

    既然目的为了让元素先出现到 RenderTree 中,和渲染相关,很容易想到可以将 setTimeout 替换成 requestAnimationFrame,这样会更精准,因为 requestAnimation 执行时机和渲染有关。

    if (!popup.classList.contains("active")) {
        popup.style.display = "block";
    
        requestAnimationFrame(() => {
            popup.classList.add("active");
        });
    }

    补充一个小插曲:在查找资料的过程中了解到 requestAnimationFrame 的规范是要求其回调函数在 Style/Layout 等阶段之前执行,起初 Chrome 和 Firefox 是遵循规范来实现的。而 Safari 和 Edge 是在执行的时机是在之后。 从现在的表现上来看,Chrome 和 Firefox 也改成了在之后执行,翻看以前的文档会说需要嵌套两层 requestAnimationFrame,现在已经不需要了。Is requestAnimationFrame called at the right point?

    优化方案三:Force Reflow

    在规范文档中,还留意到以下这句话:

    Implementations typically have a style change event to correspond with their desired screen refresh rate, and when up-to-date computed style or layout information is needed for a script API that depends on it.

    意思是说,浏览器通常还会在两种情况下会产生样式变更事件,一是满足屏幕刷新频率(不就是 requestAnimationFrame?),二是当 JS 脚本需要获取最新的样式布局信息时。

    在 JS 代码中,有些 API 被调用时,浏览器会同步地计算样式和布局,频繁调用这些 API(offset*/client*/getBoundingClientRect/scroll*/...等等)通常会成为性能瓶颈。

    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    然而在这个场景却可以产生奇妙的化学反应:

    if (!popup.classList.contains("active")) {
      popup.style.display = "block";
      popup.scrollWidth;
      popup.classList.add("active");
    }

    注意看,我们只是 display 和 add class 之间读取了一下 scrollWidth,甚至没有赋值,过渡动画就活过来了。

    트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    原因是 scrollWidth 强制同步触发了重排重绘,再下一行代码时,popup 的 display 属性已经更新到 Render Tree 上了。

    优化方案四:过渡完了告诉我 onTransitionEnd

    现在【出现】动画已经搞明白了,在看开源库的源码中发现像 vue, bootstrap, react-transition-group 等库都是使用了 force reflow 的方法,而 antd 所使用的 css-animte 库则是通过设置 setTimeout。

    【消失】动画还不够优雅,前面我们是直接写死 setTimeout 600,让元素在动画结束时消失的。这样编码可复用性差,修改动画时间还得改两处地方(JS + CSS),有没有更优雅的实现?

    popup.classList.remove("active");setTimeout(() => {
        popup.style.display = "none";
    }, 600);

    文档中也提到了 Transition Events,包括 transitionruntransitionstarttransitionendtransitioncancel,看名字就知道事件代表什么意思,这里可以用 transitionend 进行代码优化。

    if (!popup.classList.contains("active")) {
        popup.style.display = "block";
        popup.scrollWidth;
        popup.classList.add("active");
    } else {
        popup.classList.remove("active");
        popup.addEventListener(&#39;transitionend&#39;, () => {
            popup.style.display = "none";
        }, { once: true })
    }

    需要注意 transition events 同样也有冒泡、捕获的特性,如果有嵌套 transition 时需要留意 event.target

    到这里我们已经用原生 JS 完成了一个出现、消失的动画实现,完整的代码在这里。文章的最后,我们参照 vue-transition 来开发一个 React Transition 的单个元素动画过渡的最小实现。

    仿 v-transition 实现一个 React Transition 组件

    1트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    根据动画过程拆分成几个过程:

    • enter 阶段渲染 DOM 节点,初始化动画初始状态(添加 *-enter 类名)
    • enter-active 阶段执行 transition 过渡动画(添加 *-enter-active 类名)
    • enter-active 过渡完成之后进入正常展示阶段(移除 *-enter-active 类名)

    enter-to 和 leave-to 暂时用不上,leave 阶段和 enter 基本一致也不再赘述。

    直接看代码:

    export const CSSTransition = (props: Props) => {
      const { children, name, active } = props;
      const nodeRef = useRef<HTMLElement | null>(null);
      const [renderDOM, setRenderDOM] = useState(active);
    
      useEffect(() => {
        requestAnimationFrame(() => {
          if (active) {
            setRenderDOM(true);
            nodeRef.current?.classList.add(`${name}-enter`);
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            nodeRef.current?.scrollWidth;
            nodeRef.current?.classList.remove(`${name}-enter`);
            nodeRef.current?.classList.add(`${name}-enter-active`);
    
            nodeRef.current?.addEventListener("transitionend", (event) => {
              if (event.target === nodeRef.current) {
                nodeRef.current?.classList.remove(`${name}-enter-active`);
              }
            });
          } else {
            nodeRef.current?.classList.add(`${name}-leave`);
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            nodeRef.current?.scrollWidth;
            nodeRef.current?.classList.remove(`${name}-leave`);
            nodeRef.current?.classList.add(`${name}-leave-active`);
    
            nodeRef.current?.addEventListener("transitionend", (event) => {
              if (event.target === nodeRef.current) {
                nodeRef.current?.classList.remove(`${name}-leave-active`);
                setRenderDOM(false);
              }
            });
          }
        });
      }, [active, name]);
    
      if (!renderDOM) {
        return null;
      }
    
      return cloneElement(Children.only(children), {
        ref: nodeRef
      });
    };

    这个组件接收三个 props,分别是

    • children 需要做过渡动画的 ReactElement,只允许传一个 Element
    • name 过渡动画的 css 类名前缀
    • active 布尔值,用于区分是进场还是消失

    使用方式:

    <CSSTransition name="fade" active={active}>
        // 一个需要做过渡动画的 ReactElement
    </CssTransition>

    借助 transition-delay,加一点技巧实现 stagger 效果:

    1트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!

    完整的示例代码在这里,注意:这只是个快速实现用于演示的示例,有非常多的问题没有考虑在内,仅可用于学习参考。

    结语

    原本以为非常基础简单的知识点,分分钟可以写完这篇文章。没想到中途查文档,看资料,制作演示 DEMO 还是花了不少时间。好在整理资料的过程中也理清了很多知识点。希望这篇文章对你熟悉 Transition 动画有所帮助 。

    相关推荐:web前端入门视频

위 내용은 트랜지션에 대해 알고 계시나요? 전환에 대해 자세히 알아보세요!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제