Tạo một animation cho component trong React

Phân tích

See the Pen React Stripe Menu by Alex (@aholachek) on CodePen.

Trước tiên phân tích cái animation này ra thành nhiều phần để dể hình dung. Có thể xem dạng slow motion trên Codepen để xem nhé.

  • container dropdown màu trắng sẽ thay đổi kích thước và vị trí
  • background màu xám ở phía dưới của dropdown transition độ cao
  • Khi di chuyển chuột khỏi dropdown, fade out nội dung trước đó đi và chuyển vị trí nó sang element mới, sau đó đưa nội dung mới lên.

Một vài lưu ý khi làm animation với React, cứ để browser quản lý layout. Thay vì sử dụng các dropdown khác nhau, chúng ta dùng 1 dropdown và thay đổi vị trí của nó, dùng Flip technique để giả lập như có 3 dropdown khác nhau.

Dựng component thô chưa có animate

See the Pen React Stripe Menu Before Animation by Alex (@aholachek) on CodePen.

Để bắt đầu ta dựng một component navbar sử dụng styled-component

Ở đây thì chưa thêm phần gray background ở dưới dropdown, cái này nó sẽ nằm absolute

Animate với Flip technique

Chúng ta sẽ sử dụng react-flip-toolkit để thay đổi kích thước và vị trí dropdown.

<Flipper flipKey={currentIndex}>
  <Navbar>
    {navbar.Config.map((n, index) => {
      // render navbar items
    })}
  </Navbar>
</Flipper>

DropdownContainer component, đưa các element sẽ animate vào bên trong Flipped component, nhớ giá trị flipId phải khác nhau

<DropdownRoot>
  <Flipped flipId='dropdown-caret'>
    <Caret />
  </Flipped>
  <Flipped flipId='dropdown'>
    <DropdownBackground>
      {children}
    </DropdownBackground>
  </Flipped>
</DropdownRoot>

Làm 2 cái animate trên <Caret/><DropdownBackground/> riêng biệt, để thuộc tính overflow: hidden set trên <DropdownBackground /> không ảnh hưởng tới <Caret/>

Giờ có 1 vấn đề nhỏ là nội dung bên trong dropdown lúc xuất hiện bị stretch một cách kỳ cục, lý do là thuộc tính transforms: scale nó áp luôn trên children. Xử lý cái này bằng cách đưa nội dung này vào trong <Flipped/> kèm giá trị cho props inverseFlipId, lúc này children sẽ không bị effect từ thằng cha nữa, đồng thời xác định luôn là ko muốn áp thằng scale

See the Pen React Stripe Menu -- Error #1: no scale adjustment by Alex (@aholachek) on CodePen.

<DropdownRoot>
  <Flipped flipId="dropdown-caret">
    <Caret />
  </Flipped>
  <Flipped flipId="dropdown">
    <DropdownBackground>
      <Flipped inverseFlipId="dropdown" scale>
        {children}
      </Flipped>
    </DropdownBackground>
  </Flipped>
</DropdownRoot>

Chăm chúc

Cũng gần được rồi, chúng ta cần để ý thêm vào những chi tiết nhỏ nhất để animate nhìn cool hơn

Styled-components hỗ trợ rất tốt việc thay đổi keyframe animation. Chúng ta sẽ sử dụng tính năng này để làm animation lúc enter và cross-fade nội dung dropdown khi thay đổi ví trí chuột.

const getFadeContainerKeyFrame = ({ animatingOut, direction }) => {
  if (!direction) return;
  return keyframes`
    from {
      transform: translateX(${
        animatingOut ? 0 : direction === 'left' ? 20 : -20
      }px);
    }
    to {
      transform: translateX(${
        !animatingOut ? 0 : direction === 'left' ? -20 : 20
      }px);
      opacity: ${animatingOut ? 0 : 1};
    }
  `;
};

const FadeContainer = styled.div`
  animation-name: ${getFadeContainerKeyFrame};
  animation-duration: ${props => props.duration * 0.5}ms;
  animation-fill-mode: forwards;
  position: ${props => (props.animatingOut ? "absolute" : "relative")};
  opacity: ${props => (props.direction && !props.animatingOut ? 0 : 1)};
  animation-timing-function: linear;
  top: 0;
  left: 0;
`

See the Pen React Stripe Menu -- Simple FLIP by Alex (@aholachek) on CodePen.

Khi user đưa chuột lên menu mới, chúng ta ko chỉ đưa vào dropdown hiện tại mà con dropdown trước đó như children cho DropdownContainer, cùng với những thông tin về hướng di chuột của user. Sau đó DropdonwContainer sẽ wrap hết tất cả children của nó trong một component mới, FadeContents, thằng này sẽ sử dụng keyframe animation code ở trên để thêm animation tương ứng

Background animation

Cuối cùng chúng ta thêm gray background

const updateAltBackground = ({
  altBackground,
  prevDropdown,
  currentDropdown
}) => {
  const prevHeight = getFirstDropdownSectionHeight(prevDropdown)
  const currentHeight = getFirstDropdownSectionHeight(currentDropdown)
  
  // we'll use this function when we want a change 
  // to happen immediately, without CSS transitions
  const immediateSetTranslateY = (el, translateY) => {
    el.style.transform = `translateY(${translateY}px)`
    el.style.transition = "transform 0s"
    requestAnimationFrame(() => (el.style.transitionDuration = ""))
  }

  if (prevHeight) {
    // transition the grey ("alt") background from its previous height
    // to its current height
    immediateSetTranslateY(altBackground, prevHeight)
    requestAnimationFrame(() => {
      altBackground.style.transform = `translateY(${currentHeight}px)`
    })
  } else {
    // immediately set the background to the appropriate height
    // since we don't have a stored value
    immediateSetTranslateY(altBackground, currentHeight)
  }
}

See the Pen React Stripe Menu -- Animated Background by Alex (@aholachek) on CodePen.

Source: https://github.com/aholachek/react-stripe-menu

Link bài gốc của tác giả