使用场景: 随着滚动条的滚动,外层盒子sticky定住,并随着滚动条滚动切换盒子中的图片。 解决方案:
- 滚动一次,执行切换的动作,即触发后执行一次完成的切换,如下示例。使用observer监听并替换原生滚动事件实现。
// 注册插件
gsap.registerPlugin(ScrollTrigger);
// 判断是否有滚动进行中
let allowScroll = true; // sometimes we want to ignore scroll-related stuff, like when an Observer-based section is transitioning.
// 稳定滚动效果,这个延迟调用在创建后被立即暂停,所以它不会自动开始倒计时。
let scrollTimeout = gsap.delayedCall(0.8, () => (allowScroll = true)).pause(); // controls how long we should wait after an Observer-based animation is initiated before we allow another scroll-related action
// 当前滚动到的面板索引
let currentIndex = 0;
// 滚动的图片数组
let swipePanels = gsap.utils.toArray("#right-part .picture-box");
console.log("swipePanels", swipePanels);
$('#right-part').height = $('#right-part .picture-box').offsetHeight;
// set z-index levels for the swipe panels
// 给图片添加数值,用于给图片添加层级
gsap.set(swipePanels, { zIndex: (i) => swipePanels.length - i });
// create an observer and disable it to start
let intentObserver = ScrollTrigger.observe({
// 监听滚动和触摸滚动
type: "wheel,touch",
// onUp: () => allowScroll && gotoPanel(currentIndex - 1, false),
onUp: (self) => {
console.log("self.event.type", self.event.type);
if (self.event.type === "wheel") {
allowScroll && gotoPanel(currentIndex - 1, false);
} else if (self.event.type === "touchmove") {
allowScroll && gotoPanel(currentIndex + 1, true);
}
},
// onDown: () => allowScroll && gotoPanel(currentIndex + 1, true),
onDown: (self) => {
console.log("self.event.type", self.event.type);
if (self.event.type === "wheel") {
allowScroll && gotoPanel(currentIndex + 1, true);
} else if (self.event.type === "touchmove") {
allowScroll && gotoPanel(currentIndex - 1, false);
}
},
// 容忍度,防止轻微的滚动触发事件
tolerance: 10,
preventDefault: true,
onEnable(self) {
allowScroll = false;
// 重启延迟调用
scrollTimeout.restart(true);
// when enabling, we should save the scroll position and freeze it. This fixes momentum-scroll on Macs, for example.
let savedScroll = self.scrollY();
// 如果原生滚动重新定位,则强制它回到保存的位置
self._restoreScroll = () => self.scrollY(savedScroll); // if the native scroll repositions, force it back to where it should be
document.addEventListener("scroll", self._restoreScroll, {
passive: false,
});
},
// 禁用观察者时移除事件监听器
onDisable: (self) =>
document.removeEventListener("scroll", self._restoreScroll),
});
// 初始化时禁用观察者
intentObserver.disable();
// handle the panel swipe animations
function gotoPanel(index, isScrollingDown) {
// return to normal scroll if we're at the end or back up to the start
// 如果我们已经到达列表的末尾(向下滚动)或返回到开头(向上滚动),恢复正常的滚动行为
if (
(index === swipePanels.length && isScrollingDown) ||
(index === -1 && !isScrollingDown)
) {
// 禁用意图观察器,恢复原生滚动
intentObserver.disable(); // resume native scroll
return;
}
// 禁用滚动
allowScroll = false;
scrollTimeout.restart(true);
let target = isScrollingDown
? swipePanels[currentIndex]
: swipePanels[index];
gsap.to(target, {
yPercent: isScrollingDown ? -100 : 0,
duration: 0.55,
opacity:isScrollingDown ? 0 : 1,
});
currentIndex = index;
}
// pin swipe section and initiate observer
ScrollTrigger.create({
trigger: "#right-part",
pin: true,
start: "center center",
end: "+=0", // just needs to be enough to not risk vibration where a user's fast-scroll shoots way past the end
markers: true, // 显示开始和结束标记(用于调试)
onEnter: (self) => {
if (intentObserver.isEnabled) {
return;
} // in case the native scroll jumped past the end and then we force it back to where it should be.
self.scroll(self.start + 1); // jump to just one pixel past the start of this section so we can hold there.
intentObserver.enable(); // STOP native scrolling
},
onEnterBack: (self) => {
if (intentObserver.isEnabled) {
return;
} // in case the native scroll jumped backward past the start and then we force it back to where it should be.
self.scroll(self.end - 1); // jump to one pixel before the end of this section so we can hold there.
intentObserver.enable(); // STOP native scrolling
},
});
- 后续方案更改,滚动条将控制执行的整个过程,即滚动一点执行一点:
// 注册插件
gsap.registerPlugin(ScrollTrigger);
const images = gsap.utils.toArray(
".invisible-fast-focus-box #right-part .picture-box"
);
gsap.set(images, {
zIndex: (i) => -(images.length - i),
// opacity: (i) => (i === 0 ? 1 : 0),
// visibility: "hidden",
yPercent: (i) => (i === 0 ? 0 : 100),
});
/* 时间线写法在同一时间添加两个动画帧 */
let timeLine = gsap.timeline({
scrollTrigger: {
trigger: ".invisible-fast-focus-box #right-part",
// 根据索引值调整起始位置
start: () => `center center`,
end: () => (window.innerWidth > 992 ? `+=4000` : "+=1500"), // 结束位置
scrub: true,
pin: ".invisible-fast-focus-box",
// markers: true,
},
});
images.forEach((frame, index) => {
timeLine
.to(
frame,
{ yPercent: 0, ease: "power4.out", duration: index === 0 ? 0 : 3 },
// (index - 1) * 4
index * 4
)
.to(
images[index - 1] ? images[index - 1] : {},
{ opacity: 0, scale: 0.8, ease: "power4.out", duration: 3 },
// (index - 1) * 4
index * 4
)
.to({}, { duration: 1 }); // 添加1秒的空余等待时间
});
实现的方式多种多样,为了方便后续的更新迭代和更大的可操作性,推荐使用时间线形式来进行动画控制。上面代码中就是用了时间线,在当前图片切换进入时,给上一张图片添加了一段离场动画,二者通过duration持续时间和 .to 的第三个参数来控制个动画的占比。 若有不当之处欢迎指出,若有问题也可私聊探讨~