useScroll
的動畫很常使用,在之前的 Gestures 篇章只有 whileInView
來處理當滑動看到目標物並啟動動畫,但有時候我們會持續性地觸發動畫,透過滾動內容不斷給予使用者新的刺激。而 useScroll
跟 useInView
這兩個 Hooks 可以幫助我們實現目標。
useScroll
useInView
useInView
本節資源 :
施工中。
useScroll
是根據滾動時觸發動畫,最常使用在滾動視差動畫 (scrolling parallax) 以及頁面進度條 (progress) 上。它會回傳四個 motion values :
scrollX
/ scrollY
: 頁面滾動的絕對位置,單位是 px
。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
不是 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 一樣的選擇性設定 :
px
或是 %
true
為只要看見一次之後,就停止觀察,就不會再觸發"some"
| "all"
,以及數字值介於 0 至 1,被看見元素的多少比例才算 inView
。const containerRef = useRef()
const isInView = useInView(
{
root : containerRef, // viewport 容器可以是特定有卷軸的元素
margin: "0px 100px -50px 0px",
once: true,
amount : .3
}
)
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 還沒提到,像是 useCycle
與 useAnimationContorls
,會在之後的實作範例中介紹,明天會挑選 frontend mentor 的範例來做動畫效果,以實作常見的元件增添動畫。