Skip to main content

製作圖片讀取進度條與百分比


(效果預覽)

Steps Zero-核心鉤子 useOnLoadImages

傳入參數

名稱 Name型別 Type屬性 Attributes預設 Default描述 Description
refRefObject<HTMLElement>傳入想要查找 img 的外層元素

返回

一個 obj 物件

名稱 Name型別 Type屬性 Attributes預設 Default描述 Description
obj.statusbooleanimg是否全部讀取完畢
obj.loadedCountnumber目前讀取成功的img數量

程式碼

import { useState, useEffect, RefObject } from "react";

// 定義 useOnLoadImages 需要的回傳型別
interface UseOnLoadImages {
status: boolean;
loadedCount: number;
}

// 自訂 Hook: useOnLoadImages
const useOnLoadImages = (ref: RefObject<HTMLElement>): UseOnLoadImages => {
// 使用 useState 定義兩個狀態變數
const [status, setStatus] = useState(false);
const [loadedCount, setLoadedCount] = useState(0);

useEffect(() => {
// 定義一個函式,用於更新圖片載入狀態
const updateStatus = (images: HTMLImageElement[]): void => {
// 透過 map 函式檢查每個圖片是否已經載入完成
const loadedArr = images.map((image) => image.complete);
// 計算已載入完成的圖片數量
setLoadedCount(loadedArr.filter((complete) => complete).length);
// 檢查所有圖片是否都已載入完成
setStatus(loadedArr.every((item) => item));
};

// 檢查 ref 是否存在且非空
if (ref?.current === null) return;

// 取得所有在 ref 元素下的圖片元素
const imagesLoaded = Array.from(ref.current.querySelectorAll("img"));

// 若沒有圖片元素,直接將狀態設為已載入完成並返回
if (imagesLoaded.length === 0) {
setStatus(true);
return;
}

// 遍歷每個圖片元素,綁定 load 和 error 事件處理函式
imagesLoaded.forEach((image) => {
// 每次圖片載入完成或發生錯誤時,更新載入狀態
const loadedArr = imagesLoaded.map((chache) => chache.complete);

      if (loadedArr.every((item) => item)) {

        setStatus(true);

        return;

      }
image.addEventListener("load", () => updateStatus(imagesLoaded));
image.addEventListener("error", () => updateStatus(imagesLoaded));
});
});

// 返回狀態變數
return { status, loadedCount };
};

export default useOnLoadImages;

使用這個 Hook 追蹤指定元素下圖片的載入狀態。在 Hook 內部,透過 useState 定義了兩個狀態變數:

  1. status(全部圖片是否載入完成)
  2. loadedCount(目前成功載入多少圖片)

接著使用 useEffect 監聽指定元素下圖片載入的事件。
useEffect 內部,先檢查指定的元素是否存在,若不存在則直接返回。接著取得指定元素下的所有圖片元素,若沒有圖片元素則直接將載入狀態設為已完成。如果有圖片元素,則遍歷每個圖片元素,綁定 loaderror 事件的處理函式,以便在圖片載入完成或發生錯誤時更新載入狀態。

最後,將 statusloadedCount 返回作為 Hook 的結果。這樣使用該 Hook 的組件就可以獲取到圖片載入的狀態和已載入完成的圖片數量。

Steps One-使用方式 1,百分比範例

以製作百分比為例,最簡單的方式,可以直接搭配 gsap.utils.mapRange 來將 loadedCount 映射成百分比

關於 mapRange 使用方式可參考我的另一篇筆記,

GSAP utils_使用 gsap.utils.mapRange 映射數值與滑鼠追蹤

基本寫法

import useOnLoadImages from "./hooks/useOnLoadImages";
import { gsap } from "gsap";

const data = [
"https://i.imgur.com/5UmqIwL.jpg",
"https://i.imgur.com/mJ2RYpY.jpg",
"https://i.imgur.com/oFOt3CA.jpg",
"https://i.imgur.com/fD9NgFo.png",
"https://i.imgur.com/mrlA7SC.png",
"https://i.imgur.com/3qJXBPV.png",
"https://i.imgur.com/DeF14KL.jpg",
];


function App(){
const { status, loadedCount } = useOnLoadImages(imgWrapperRef);
return (
<>
<p className="mb-10 text-center font-dela text-8xl text-yellow-400">
{Math.round(gsap.utils.mapRange(0, 7, 0, 100, loadedCount))}%
</p>
<ul ref={imgWrapperRef} className="mx-auto max-w-2xl">
{data.map((img) => (
<li key={img}>
<img src={img} alt="" />
</li>
))}
</ul>
</>
)
}

這裡的參數 7 是假定我已經知道圖片數量有 7 個

gsap.utils.mapRange(0, 7, 0, 100, loadedCount)

串接 api 時寫法

在串接 api 時,我們總是無法提前知道圖片有幾張,因此我們可以使用 useEffect 或其他框架所提供 onMounted 的鉤子來抓取 img 數量

import React, { useEffect, useRef, useState } from "react";
import useOnLoadImages from "./hooks/useOnLoadImages";
import { gsap } from "gsap";

const data = [
"https://i.imgur.com/5UmqIwL.jpg",
"https://i.imgur.com/mJ2RYpY.jpg",
"https://i.imgur.com/oFOt3CA.jpg",
"https://i.imgur.com/fD9NgFo.png",
"https://i.imgur.com/mrlA7SC.png",
"https://i.imgur.com/3qJXBPV.png",
"https://i.imgur.com/DeF14KL.jpg",
];


function App(){
const imgWrapperRef = useRef<HTMLUListElement>(null);
const [loadedPercent, setLoadedPercent] = useState(0);
const { status, loadedCount } = useOnLoadImages(imgWrapperRef);

useEffect(() => {
if (imgWrapperRef.current === null) return;
const imgCounts = imgWrapperRef.current.querySelectorAll("img").length;
setLoadedPercent(Math.round(gsap.utils.mapRange(0, imgCounts, 0, 100, loadedCount))
);
},[])
return (
<>
<p className="mb-10 text-center font-dela text-8xl text-yellow-400">
{loadedPercent}%
</p>
<ul ref={imgWrapperRef} className="mx-auto max-w-2xl">
{data.map((img) => (
<li key={img}>
<img src={img} alt="" />
</li>
))}
</ul>
</>
)
}

Steps Two - 使用方式 2,讀取進度條

1. 宣告下列 ref 用於控制進度條動畫

/* mainCtx 用來作為 GSAP 動畫的上下文或其他與 main 元素相關的操作。*/
const mainCtx = useRef(null);

/* tlRef 是一個對 GSAP 時間軸的參考,*/
// 1. 初始化選項 { paused: true } 確保時間軸創建時將不會自動播放。
// 2. 用於控制進度條長度變化
const tlRef = useRef(gsap.timeline({ paused: true }));

/* tlCtrlRef 用於控制 tlRef 的播放進度 */
const tlCtrlRef = useRef(gsap.timeline());

/* previousProgress 儲存進度條的上一個百分比值 */
const previousProgress = useRef(0);

2. 在 useEffect 建立進度條動畫

useEffect(() => {
if (mainCtx.current === null || imgWrapperRef.current === null) return;

// 1. 獲取圖片數量
const imgCounts = imgWrapperRef.current.querySelectorAll("img").length;


// 2. 用於更新 GSAP 時間軸的進度。動畫的核心邏輯在這裡:
const progress = gsap.utils.mapRange(0, imgCounts, 0, 1, loadedCount);


const ctx = gsap.context(() => {


// 3. 建立進度條形變動畫,
// 由於此時間軸初始值在 useRef 設定為 { pause: true } ,因此不會執行動畫
tlRef.current.to("#js-progress", {
right: "0%",
});


// 4. 控制 tlRef 播放進度,使用 previousProgress 與 progress 建立過渡動畫
tlCtrlRef.current
.to(tlRef.current, {
progress: previousProgress.current,
})
.to(tlRef.current, {
progress: progress,
ease: "none",
});

previousProgress.current = progress;
}, mainCtx.current);

return () => ctx.revert();
}, [loadedCount]);

  return (
<main ref={mainCtx}>
<section className="min-h-screen pt-10">
<h1 className="relative mx-auto mb-10 w-max border-2 border-black bg-white px-10 py-5 text-center font-dela text-3xl after:absolute after:left-2 after:top-2 after:-z-10 after:block after:h-full after:w-full after:bg-blue-600 after:content-['']">
React+GSAP實作圖片讀取進度條
</h1>
<p className="mb-10 text-center font-dela text-8xl text-yellow-400">
{loadedPercent}%
</p>
<div className="relative mx-auto mb-10 h-2 w-full max-w-2xl">
<div
id="js-progress"
className="absolute left-0 right-[100%] h-2 bg-yellow-500 hover:right-0"
/>
</div>
<ul ref={imgWrapperRef} className="mx-auto max-w-2xl">
{data.map((img) => (
<li key={img}>
<img src={img} alt="" />
</li>
))}
</ul>
</section>
</main>
);

參考文章

How to detect images loaded in React