iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0
自我挑戰組

向網頁施點魔法粉 framer-motion 系列 第 15

#15 Bump in to you - useScroll & useInView

  • 分享至 

  • xImage
  •  

useScroll 的動畫很常使用,在之前的 Gestures 篇章只有 whileInView 來處理當滑動看到目標物並啟動動畫,但有時候我們會持續性地觸發動畫,透過滾動內容不斷給予使用者新的刺激。而 useScrolluseInView 這兩個 Hooks 可以幫助我們實現目標。

目錄

  1. 翻滾吧 : useScroll
  2. 看在眼裡 : useInView
  3. 連動起來 : 導覽列與 useInView

本節資源 :
施工中。

翻滾吧 : useScroll

useScroll 是根據滾動時觸發動畫,最常使用在滾動視差動畫 (scrolling parallax) 以及頁面進度條 (progress) 上。它會回傳四個 motion values :

  1. scrollX / scrollY : 頁面滾動的絕對位置,單位是 px
  2. scrollXProgress / scrollYProgress : 數值介於 0 到 1,是定義的目標 (起點) 到目前目標所在位置的差距,更簡單的說你滑到容器內容的幾分之幾了。

useScroll 預設會追蹤 (整體頁面) 目前的滾輪位置 :

const { scrollY } = useScroll()

useEffect(() => {
  return scrollY.onChange((latest) => {
    console.log("Page scroll: ", latest)
  })
}, [])

useScroll 設定 :

  • container : 指定元素作為容器,必須要有卷軸 overflow : scroll,可以利用 ref 來拿到容器實例,進而加入 useScroll 裡。
  • target : 指定元素的 ref 。預設情況下,容器的可滾動區域,也就是整個容器。但也可以設置為另一個元素,觀察元素在容器 viewport 中的 progress。
  • axis : 預設是 y 軸,指定偵測的卷軸方向。

進度條

除了最基本的進度條伸縮,也可以利用 useSpring 為 motion value 增添彈簧動畫,增加絲滑流暢的感覺 :


export default function ProgressBarWithUseScroll() {
    // 因為在個別範例,這邊以容器為主
    const container = useRef();

    const { scrollYProgress } = useScroll({
        container,
    });
    
    // 讓 scaleX 根據 scrollYProgress 富有彈簧動畫
    const scaleX = useSpring(scrollYProgress, {
        stiffness: 100,
        damping: 30,
        restDelta: 0.001,
    });

    return (
        <>
            <h3>進度條</h3>
            <div
                ref={container}
                style={{
                    position: "relative",
                    width: 300,
                    height: 300,
                    border: "1px solid #000",
                    // scroll 條件
                    overflowY: "scroll",
                    overflowX: "hidden",
                }}
            >
                <motion.div
                    style={{
                        height: 10,
                        // 如果是整個頁面可以用 fixed,個別可以用 sticky
                        position: "sticky",
                        zIndex: 2,
                        top: 0,
                        left: 0,
                        right: 0,
                        backgroundColor: "#fa0",
                        scaleX,
                        // 校正起點指向左上角
                        transformOrigin: "0%",
                    }}
                />
                {Array.from({ length: 10 }).map((_, index) => (
                    <motion.p
                        key={index}
                        style={{
                            padding: "0em 1em",
                            marginTop: index > 0 && 100,
                        }}
                        initial={{
                            opacity: 0,
                            x: "-100%",
                        }}
                        whileInView={{
                            opacity: 1,
                            x: 0,
                        }}
                        viewport={{ once: true }}
                    >
                      {/* 文字內容 */}
                    </motion.p>
                ))}
            </div>
        </>
    );
}

  • 效果 :

看在眼裡 : useInView

useInView 不是 motion value 的一部分,但它的功能跟滾輪操作很有關係,在 Gestures 中只有使用到 whileInView 進行操作,但沒有像 Intersection Observation API 一樣有 observer 可以進一步判斷追加其他的邏輯,而 useInView 就是為補足這一方面而生。

它是一個很小的東西,只是單純的 React state,最主要做一件是 : 檢查元件是不是在 (容器) viewport 中 ,等同於 Intersection Observation API 中的 observer.isIntersecting()

// 可以判斷東西是否有進入 viewport
useEffect(() => {
  console.log("Element is in view: ", isInView)
}, [isInView])

此外還有些跟 JS 原生 API 一樣的選擇性設定 :

  • root : 指定可視的卷軸容器。
  • margin : viewport 跟目標的邊距,單位一定要是 px 或是 %
  • once : 布林值,true 為只要看見一次之後,就停止觀察,就不會再觸發
  • amount : 有 "some" | "all" ,以及數字值介於 0 至 1,被看見元素的多少比例才算 inView
const containerRef = useRef()
const isInView = useInView(
  {
    root : containerRef, // viewport 容器可以是特定有卷軸的元素
    margin: "0px 100px -50px 0px",
    once: true, 
    amount : .3
  }
)

連動起來 : 導覽列與 useInView

  • 可以做到滾到特定區塊進行導覽列動畫
    (修正一些問題中)
  • 效果 :
  • 這是我以前使用 Intersection Observation API 的自製的 useScrollSpy ,用來監聽目前頁面區塊改變導覽列元素的顏色 :
export function useScrollspy() {
    // 當前區塊錨點
    const [nowAnchor, setnowAnchor] = useState(null);
    // 標示所有的區塊
    const sectionrefs = useRef([]);

    // 在 ref 裡面在裝 ref
    sectionrefs.current = Array.from({ length: 5 }).map(
        (_, i) => sectionrefs.current[i] || createRef()
    );

    useEffect(() => {
        const options = {
            rootMargin: "0px",
            threshold: 0.1,
        };
        // 處理邏輯
        const handler = (entries, observer) => {
            entries.forEach((entry) => {
                if (!entry.isIntersecting) return;
                setnowAnchor(entry.target.id);
            });
        };
        // 建立 IntersectionObserver 實例
        let observer = new IntersectionObserver(handler, options);

        // 每一個元素加入觀察
        sectionrefs.current.forEach((ref) => {
            if (ref.current) {
                observer.observe(ref.current);
            }
        });

        return () => {
            observer.disconnect();
        };
    }, []);

    // 飛到指定區塊
    const scroll2Section = (anchor) => {
        if (nowAnchor === anchor) return;
        // 從 anchor (id) 找到區塊的實體
        const trigger = sectionrefs.current.find((section) 
          => section.current.id === anchor
        );
        // scrollIntoView 是 JS 原生滑到區塊的好用 API
        trigger.current.scrollIntoView({
            behavior: "smooth",
        });
    };
    return { sectionrefs, nowAnchor, scroll2Section };
}

總結

還有些 Utilities Hooks 還沒提到,像是 useCycleuseAnimationContorls,會在之後的實作範例中介紹,明天會挑選 frontend mentor 的範例來做動畫效果,以實作常見的元件增添動畫。

參考資料

  1. 官方文件 ─ useScroll :useScroll | Framer for Developers
  2. 官方文件 ─ useInView : useInView | Framer for Developers

上一篇
#14 Move & Bounce Again - useTranfrom & useSpring
系列文
向網頁施點魔法粉 framer-motion 15
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言