iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0

元件介紹

Skeleton 是一個骨架載入元件(Skeleton Screen Loading),跟 Spin 不同的是,Skeleton 幫助我們在頁面載入完成之前可以先看到一個描繪當前頁面大致框架的樣式,載入完成之後,原本骨架的地方將被真實的資料給替換。

在資料載入之前,先用色塊來代替文字和圖片,將頁面架構和資料位置呈現在使用者眼前,可以讓使用者產生一種「內容即將要出現」的感覺,而非將注意力全放在「等待時間」上。因此使用者體驗上會比較好,而且也可以減少 loading icon 突然變成資料區塊的不連續突兀感。適合使用在整個區塊需要載入的狀況。

參考設計 & 屬性分析

為了達到不同的效果,各家的做法也不一,因此以下分享幾個在網路上人家分享的做法,不一定盡善盡美,但覺得也是看到了不同的思維可以學習。

無動畫純色塊

這部分是最單純,就是用色塊勾勒出頁面顯示的架構。

const SkeletonWrapper = styled.div`
  display: flex;
  align-items: center;
  & > *:not(:first-child) {
    margin-left: 16px;
  }
`;

const TextLineWrapper = styled.div`
  & > *:not(:first-child) {
    margin-top: 12px;
  }
`;

const TextLine = styled.div`
  background: #EEE;
  height: 12px;
`;

const Avatar = styled.div`
  width: 50px;
  height: 50px;
  background: #EEE;
`;


const Skeleton = () => (
  <SkeletonWrapper>
    <Avatar />
    <TextLineWrapper>
      <TextLine style={{ width: 300 }} />
      <TextLine style={{ width: 230 }} />
    </TextLineWrapper>
  </SkeletonWrapper>
);

閃爍色塊

因為前面色塊真的太單純,可能畫面會呆呆的,不太知道畫面是有在載入還是當掉,可以可以簡單加一些動畫,直接用顏色深淺的變化來做。

我目前做的範例是跟先前一樣,只是加入一個閃爍的動畫,整體的感覺就很不一樣。我用的動畫只有單純改變 opacity,然後動畫的設定是用 alternate-reverse,可以像是乒乓球一樣來回播放,這樣看起來比較不會卡頓,最後是 infinite 讓他無限輪播。

附註:我用的 gif 動畫跑得不是很順,讀者可以上我的 storybook 看。

const flash = keyframes`
  from {
    opacity: 0.3;
  }
  to {
    opacity: 1;
  }
`;

const TextLine = styled.div`
  background: #EEE;
  height: 12px;
  animation: ${flash} 0.8s ease-in-out alternate-reverse infinite;
`;

const Avatar = styled.div`
  width: 50px;
  height: 50px;
  background: #EEE;
  animation: ${flash} 0.8s ease-in-out alternate-reverse infinite;
`;

背景光暈移動動畫

Antd 也是比較接近下面這種動畫,就是我們可以看到一個陰影或是光暈的東西在前面滑過,有點像是光源移動的感覺。


https://medium.com/d-d-mag/protopie-實驗室-skeleton-loading-63d3a939a962

其實原理如這張動畫:

做法的概念簡單來說,就是我們要製造的一個剪層色塊在背景從左到右滑動,然後不間斷地輪播就可以了,然後漸層超出背景的地方,就用 overflow: hidden; 處理掉。

下面這邊就是我的作法:

const slide = keyframes`
  from {
    left: -150%;
  }
  to {
    left: 100%;
  }
`;

const StyledBackgroundSlide = styled.div`
  width: 12px;
  height: 12px;
  background: #EEE;
  position: relative;
  overflow: hidden;
  &:before {
    content: '';
    position: absolute;
    height: 100%;
    width: 80px;
    top: 0px;
    background: linear-gradient(to right, transparent 0%, #FFFFFF99 50%, transparent 100%);
    animation: ${slide} 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
    box-shadow: 0 4px 10px 0 #FFFFFF33;
  }
`;

const BackgroundSlide = (props) => (
  <StyledBackgroundSlide {...props} />
);

我的背景是用 <div />,然後漸層色塊我用一個偽元素 :before 來做,主要是利用 linear-gradient 來製造漸層的背景,最後加上一點 box-shadow 來讓這個漸層更延伸,好像有點光暈感,看起來比較自然一些。

介面設計

屬性 說明 類型 默認值
variant 變化模式 colorBlock, flash, slide slide

元件實作

要元件實作的時候發現我們上面在分析解釋就已經講完了 XD

所以上述三種不同動畫樣式我乾脆就把他包做一個元件,然後用 variant 來決定要呈現哪一種動畫,像是下面這樣:

<Skeleton variant="slide" />

然後根據我們不同的頁面架構,我們再來勾勒出它的形狀,例如像上面一個 Avatar 搭配兩行字的架構,我們就能夠用下面的方式來做:

import Skeleton from '../components/Skeleton';

const Avatar = ({ style, ...props }) => (
  <Skeleton style={{ width: 50, height: 50, ...style }} {...props} />
);

const TextLine = ({ style, ...props }) => (
  <Skeleton style={{ width: 50, height: 12, ...style }} {...props} />
);

const SkeletonDemo = ({ variant }) => (
  <SkeletonWrapper>
    <Avatar variant={variant} />
    <TextLineWrapper>
      <TextLine variant={variant} style={{ width: 300 }} />
      <TextLine variant={variant} style={{ width: 230 }} />
    </TextLineWrapper>
  </SkeletonWrapper>
);

所以我們這邊準備的是一個最基礎單元的 Skeleton,那如果我們需要一篇文章的 Skeleton,或是一張卡片的 Skeleton,那我們再用這個小單元來組成就可以了,那如果很重複使用這些大單元,我們就能夠再額外把他包成一個元件,例如:<ArticleSkeleton />, <CardSkeleton />...等等。


Skeleton 元件原始碼:
Source code

Storybook:
Skeleton

參考

https://medium.com/d-d-mag/protopie-實驗室-skeleton-loading-63d3a939a962

https://w3c.hexschool.com/blog/24db18f8

https://betterprogramming.pub/the-what-why-and-how-of-using-a-skeleton-loading-screen-e68809d7f702

https://medium.com/tiffrrr/css-使用background-animation-製作skeleton-loading-screen-f68fb307525c


上一篇
【Day24】反饋元件 - Spin
下一篇
【Day26】反饋元件 - Progress bar
系列文
30 天擁有一套自己手刻的 React UI 元件庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言