这是我们创建完美轮播教程系列的第三部分,也是最后一部分。在第 1 部分中,我们评估了 Netflix 和亚马逊上的轮播,这两个是世界上使用最频繁的轮播。我们设置了轮播并实现了触摸滚动。
然后在第 2 部分中,我们添加了水平鼠标滚动、分页和进度指示器。繁荣。
现在,在我们的最后一部分中,我们将研究键盘可访问性的阴暗和经常被遗忘的世界。当视口大小发生变化时,我们将调整代码以重新测量轮播。最后,我们将使用弹簧物理进行一些收尾工作。
您可以使用此 CodePen从我们离开的地方继续。
键盘辅助功能
确实,大多数用户不依赖键盘导航,所以遗憾的是,我们有时会忘记我们的用户。在某些***/地区,使网站无法访问可能是非法的。但更糟糕的是,这是一个鸡巴的举动。
好消息是它通常很容易实现!事实上,浏览器为我们完成了大部分工作。说真的:尝试通过我们制作的旋转木马进行切换。因为我们使用了语义标记,所以您已经可以了!
除了,您会注意到,我们的导航按钮消失了。这是因为浏览器不允许关注视口之外的元素。所以即使我们已经overflow: hidden
设置了,我们也无法水平滚动页面;否则,页面确实会滚动以显示具有焦点的元素。
这没关系,在我看来,它有资格作为“可用”,尽管并不完全令人愉快。
Netflix 的轮播也以这种方式工作。但是因为他们的大多数标题都是延迟加载的,而且它们也可以通过键盘被动访问(这意味着他们没有专门编写任何代码来处理它),我们实际上无法选择除了我们已经拥有的少数标题之外的任何标题已经加载。它看起来也很糟糕:
我们可以做得更好。
处理focus
事件
为此,我们将*** focus
在轮播中的任何项目上触发的事件。当一个项目获得焦点时,我们将查询它的位置。sliderX
然后,我们将检查该 sliderVisibleWidth
项目是否在可见窗口内。如果不是,我们将使用我们在第 2 部分中编写的相同代码对其进行分页。
在 carousel
函数的最后,添加这个事件监听器:
slider.addeventListener('focus', onFocus, true);
您会注意到我们提供了第三个参数,true
. 我们可以使用所谓的 事件委托,而不是为每个项目添加事件***器 只听一个元素上的事件,它们的直接父元素。该 focus
事件不会冒泡,因此 true
告诉事件***器*** 捕获阶段,在该阶段事件在从 window
通过到目标的每个元素上触发(在本例中,项目接收焦点)。
在我们不断增长的事件***器块之上,添加 onFocus
函数:
function onFocus(e) { }
在本节的剩余部分,我们将使用这个函数。
我们需要测量项目的left
偏移right
量并检查任一点是否位于当前可视区域之外。
该项目由事件的target
参数提供,我们可以用 来衡量它getBoundingClientRect
:
const { left, right } = e.target.getBoundingClientRect();
left
并且 right
相对于 viewport,而不是滑块。所以我们需要得到轮播容器的 left
偏移量来解决这个问题。在我们的示例中,这将是 0
,但为了使轮播变得健壮,它应该考虑放置在任何地方。
const carouselLeft = container.getBoundingClientRect().left;
现在,我们可以做一个简单的检查,看看项目是否在滑块的可见区域之外,并朝那个方向分页:
if (left < carouselLeft) { gotoPrev(); } else if (right > carouselLeft + sliderVisibleWidth) { gotoNext(); }
现在,当我们四处切换时,轮播会自信地用我们的键盘焦点进行分页!只需几行代码,就可以向我们的用户表达更多的爱。
重新测量旋转木马
当您按照本教程进行操作时,您可能已经注意到,如果您调整浏览器视口的大小,轮播将不再正确分页。这是因为我们在初始化时只测量了它相对于可见区域的宽度一次。
为了确保我们的轮播功能正确,我们需要将一些测量代码替换为一个新的事件监听器,该监听器会在调整大小时触发window
。
现在,在您的 carousel
函数开始附近,就在我们定义 的行之后 progressBar
,我们想用 替换其中的三个 const
测量值 let
,因为我们将在视口更改时更改它们:
const totalItemsWidth = getTotalItemsWidth(items); const maxXOffset = 0; let minXOffset = 0; let sliderVisibleWidth = 0; let clampXOffset;
然后,我们可以将之前计算这些值的逻辑移到一个新 measureCarousel
函数中:
function measureCarousel() { sliderVisibleWidth = slider.offsetWidth; minXOffset = - (totalItemsWidth - sliderVisibleWidth); clampXOffset = clamp(minXOffset, maxXOffset); }
我们想立即调用这个函数,所以我们仍然在初始化时设置这些值。在下一行,调用 measureCarousel
:
measureCarousel();
旋转木马应该和以前一样工作。要更新窗口调整大小,我们只需在 carousel
函数的最后添加这个事件监听器:
window.addEventListener('resize', measureCarousel);
现在,如果您调整轮播的大小并尝试分页,它将继续按预期工作。
性能说明
值得考虑的是,在现实世界中,您可能在同一页面上有多个轮播,将此测量代码的性能影响乘以该数量。
正如我们在第 2 部分中简要讨论的那样,频繁地执行繁重的计算是不明智的。对于指针和滚动事件,我们说过您希望每帧执行一次以帮助保持 60fps。调整大小事件略有不同,因为整个文档将重排,这可能是网页将遇到的资源最密集的时刻。
在用户完成窗口大小调整之前,我们不需要重新测量轮播,因为在此期间他们不会与之交互。我们可以将我们的measureCarousel
函数包装在一个名为 debounce的特殊函数中。
x
debounce 函数本质上说:“只有在超过毫秒内没有被调用时才触发这个函数。” 您可以在David Walsh 的优秀入门书上阅读更多关于 debounce的信息,也可以获取一些示例代码。
收尾工作
到目前为止,我们已经创建了一个非常好的轮播。它易于访问,动画效果很好,它可以跨触摸和鼠标工作,并且它以原生滚动轮播不允许的方式提供了很大的设计灵活性。
但这不是“创建一个非常好的轮播”教程系列。是时候让我们炫耀一下了,要做到这一点,我们有一个秘密武器。 弹簧。
我们将使用弹簧添加两个交互。一种用于触摸,一种用于分页。他们都会以一种有趣和好玩的方式让用户知道他们已经到达了轮播的尽头。
触摸弹簧
首先,当用户试图将滑块滚动到其边界之外时,让我们添加一个ios样式的拖轮。目前,我们正在使用 clampXOffset
. 相反,让我们用一些代码替换它,当计算的偏移量超出其边界时应用拖船。
首先,我们需要导入我们的弹簧。有一个叫做 变压器nonlinearSpring
的变压器,它对我们提供的数字施加指数增加的力,朝向 origin
. 这意味着我们将滑块拉得越远,它就会向后拉得越多。我们可以像这样导入它:
const { applyOffset, clamp, nonlinearSpring, pipe } = transform;
在 determineDragDirection
函数中,我们有以下代码:
action.output(pipe( ({ x }) => x, applyOffset(action.x.get(), sliderX.get()), clampXOffset, (v) => sliderX.set(v) ));
就在它上面,让我们创建两个弹簧,一个用于轮播的每个滚动限制:
const elasticity = 5; const tugLeft = nonlinearSpring(elasticity, maxXOffset); const tugRight = nonlinearSpring(elasticity, minXOffset);
决定一个价值 elasticity
是一个玩弄,看看什么感觉正确的问题。数字太低,弹簧感觉太硬。太高你不会注意到它的拖拽,或者更糟糕的是,它会将滑块推离用户手指更远的地方!
现在我们只需要编写一个简单的函数,如果提供的值超出允许的范围,它将应用这些弹簧之一:
const applySpring = (v) => { if (v > maxXOffset) return tugLeft(v); if (v < minXOffset) return tugRight(v); return v; };
我们可以 clampXOffset
在上面的代码中用 applySpring
. 现在,如果您将滑块拉过它的边界,它会向后拉!
然而,当我们放开弹簧时,它会毫不客气地弹回原位。我们想要修改 stopTouchScroll
当前处理动量滚动的函数,以检查滑块是否仍在允许的范围之外,如果是,则应用一个带有 physics
动作的弹簧。
春季物理学
该 physics
动作也能够模拟弹簧。我们只需要为它提供 spring
和 to
属性。
在 stopTouchScroll
中,将现有的滚动 physics
初始化移动到确保我们处于滚动限制范围内的一段逻辑中:
const currentX = sliderX.get(); if (currentX < minXOffset || currentX > maxXOffset) { } else { action = physics({ from: currentX, velocity: sliderX.getVelocity(), friction: 0.2 }).output(pipe( clampXOffset, (v) => sliderX.set(v) )).start(); }
在 if
语句的第一个子句中,我们知道滑块超出了滚动限制,因此我们可以添加我们的弹簧:
action = physics({ from: currentX, to: (currentX < minXOffset) ? minXOffset : maxXOffset, spring: 800, friction: 0.92 }).output((v) => sliderX.set(v)) .start();
我们想创造一个感觉活泼和反应灵敏的弹簧。我选择了一个相对较高的 spring
值来获得一个紧密的“pop”,并且我已经降低了 friction
to 0.92
以允许一点反弹。您可以将其设置为 1
完全消除反弹。
作为一项功课,尝试将 滚动 clampXOffset
的函数中的 in 替换为当 x 偏移量达到其边界时触发类似弹簧的函数。而不是当前的突然停止,试着让它在最后轻轻地反弹。output
physics
分页弹簧
触摸用户总是得到春天的好处,对吧?让我们通过检测轮播何时达到其滚动限制,并通过指示性拖拽向用户清晰而自信地显示他们已经结束,来与桌面用户分享这种喜爱。
首先,我们要在达到限制时禁用分页按钮。让我们首先添加一个css规则来设置按钮样式以显示它们是 disabled
. 在 button
规则中,添加:
transition: background 200ms linear; &.disabled { background: #eee; }
我们在这里使用一个类而不是更多语义disabled
属性,因为我们仍然想捕获点击事件,顾名思义,disabled
这会阻塞。
将此类添加 disabled
到 Prev 按钮,因为每个轮播都以 0
偏移开始:
<button class="prev disabled">Prev</button>
在顶部carousel
,创建一个名为 的新函数 checkNavButtonStatus
。我们希望这个函数简单地检查提供的值 minXOffset
, maxXOffset
并相应地设置按钮 disabled
类:
function checkNavButtonStatus(x) { if (x <= minXOffset) { nextButton.classList.add('disabled'); } else { nextButton.classList.remove('disabled'); if (x >= maxXOffset) { prevButton.classList.add('disabled'); } else { prevButton.classList.remove('disabled'); } } }
每次 sliderX
更改时都会很想调用它。如果我们这样做了,只要弹簧在滚动边界周围摆动,按钮就会开始闪烁。也会导致诡异 如果在其中一个弹簧动画期间按下其中一个按钮时的行为。如果我们在旋转木马的末端,“滚动末端”拖船应该总是触发,即使有一个弹簧动画将它从绝对末端拉开。
所以我们需要更有选择性地选择何时调用这个函数。将其称为:
在 的最后一行 onWheel
,添加 checkNavButtonStatus(newX);
。
在 的最后一行 goto
,添加 checkNavButtonStatus(targetX);
。
最后,在 的末尾 determineDragDirection
和动量滚动子句( 中的代码else
)中stopTouchScroll
,替换:
(v) => sliderX.set(v)
和:
(v) => { sliderX.set(v); checkNavButtonStatus(v); }
现在剩下的就是修改 gotoPrev
并 gotoNext
检查他们的触发按钮的 classList disabled
并且只有在它不存在时才分页:
const gotoNext = (e) => !e.target.classList.contains('disabled') ?goto(1) : notifyEnd(-1, maxXOffset); const gotoPrev = (e) => !e.target.classList.contains('disabled') ?goto(-1) : notifyEnd(1, minXOffset);
该 notifyEnd
函数只是另一个 physics
弹簧,它看起来像这样:
function notifyEnd(delta, targetOffset) { if (action) action.stop(); action = physics({ from: sliderX.get(), to: targetOffset, velocity: 2000 * delta, spring: 300, friction: 0.9 }) .output((v) => sliderX.set(v)) .start(); }
试一试,然后再 physics
根据自己的喜好调整参数。
只剩下一个小虫子了。当滑块超出其最左侧边界时,进度条正在反转。我们可以通过替换来快速解决这个问题:
progressBarRenderer.set('scaleX', progress);
和:
progressBarRenderer.set('scaleX', Math.max(progress, 0));
我们 可以 防止它以另一种方式弹跳,但我个人认为它反映了弹簧运动非常酷。当它从里到外翻转时,它看起来很奇怪。
清理自己之后
使用单页应用程序,网站在用户会话中的持续时间更长。通常,即使“页面”发生变化,我们仍在运行与初始加载时相同的 JS 运行时。我们不能每次用户点击一个链接时都依赖一个干净的状态,这意味着我们必须自己清理以防止事件监听器触发死元素。
在react中,此代码放置在 componentWillLeave
方法中。vue使用 beforeDestroy
. 这是一个纯 JS 实现,但我们仍然可以提供在任一框架中同样有效的 destroy 方法。
到目前为止,我们的 carousel
函数还没有返回任何东西。让我们改变它。
首先,将最后一行,即调用 的行更改 carousel
为:
const destroyCarousel = carousel(document.queryselector('.container'));
我们将只返回一个东西 from carousel
,一个解除绑定我们所有事件监听器的函数。在 carousel
函数的最后,写:
return () => { container.removeEventListener('touchstart', startTouchScroll); container.removeEventListener('wheel', onWheel); nextButton.removeEventListener('click', gotoNext); prevButton.removeEventListener('click', gotoPrev); slider.removeEventListener('focus', onFocus); window.removeEventListener('resize', measureCarousel); };
现在,如果您打电话 destroyCarousel
并尝试玩转盘,什么也不会发生!看到这个样子,几乎有点难过。
就是这样
唷。那是很多!我们已经走了多远。你可以在这个 CodePen看到成品。在最后一部分中,我们添加了键盘可访问性、在视口变化时重新测量轮播、弹簧物理的一些有趣的添加,以及再次将其全部拆除的令人心碎但必要的步骤。
- 处理focus 事件
- 性能说明
- 触摸弹簧
- 春季物理学
- 分页弹簧