欢迎回到创建完美轮播教程系列。我们正在使用javascript和Popmotion的物理、补间和输入跟踪功能制作一个可访问且令人愉悦的轮播。
在我们教程的第 1 部分中,我们了解了 Amazon 和 Netflix 如何创建轮播并评估其方法的优缺点。根据我们的学习,我们决定了轮播的策略,并使用物理实现了触摸滚动。
在第 2 部分中,我们将实现水平鼠标滚动。我们还将研究一些常见的分页技术并实施一种。最后,我们将连接一个进度条,指示用户在轮播中的距离。
你可以通过打开这个 CodePen来恢复你的保存点,它会从我们离开的地方开始。
水平鼠标滚动
JavaScript 轮播很少尊重水平鼠标滚动。这是一种耻辱:在实现基于动量的水平滚动的笔记本电脑和鼠标上,这是迄今为止导航旋转木马的最快方式。这就像强迫触摸用户通过按钮而不是滑动来导航一样糟糕。
幸运的是,它只需几行代码即可实现。在 carousel
函数的最后,添加一个新的事件监听器:
container.addeventListener('wheel', onWheel);
在您的事件下方 startTouchScroll
,添加一个名为的存根函数 onWheel
:
function onWheel(e) { console.log(e.deltaX) }
现在,如果您在旋转木马上运行滚轮并检查控制台面板,您将在 x 轴输出上看到滚轮距离。
与触摸一样,如果滚轮的移动大部分是垂直的,则页面应该像往常一样滚动。如果它是水平的,我们想要捕捉轮子的运动并将其应用于轮播。因此,在 中onWheel
,将 替换为console.log
:
const angle = calc.angle({ x: e.deltaX, y: e.deltaY }); if (angleIsVertical(angle)) return; e.stopPropagation(); e.preventDefault();
如果滚动是水平的,此代码块将停止页面滚动。更新滑块的 x 偏移量现在只需获取事件的deltaX
属性并将其添加到我们的当前 sliderX
值:
const newX = clampXOffset( sliderX.get() + - e.deltaX ); sliderX.set(newX);
我们正在重用之前的 clampXOffset
函数来包装这个计算,并确保轮播不会滚动到其测量的边界之外。
关于限制滚动事件的旁白
任何处理输入事件的好教程都会解释限制这些事件的重要性。这是因为滚动、鼠标和触摸事件都可以比设备的帧速率更快地触发。
您不想执行不必要的资源密集型工作,例如在一帧中两次渲染轮播,因为这是一种资源浪费,也是一种快速制作感觉迟钝的界面的方法。
本教程没有涉及到这一点,因为 Popmotion 提供的渲染器实现了Framesync,一个小型的帧同步作业调度程序。这意味着您可以(v) => sliderRenderer.set('x', v)
连续调用多次,而昂贵的渲染只会在下一帧发生一次。
分页
滚动完成。现在我们需要为迄今为止不受欢迎的导航按钮注入一些活力。
现在,本教程是关于交互的,所以请随意设计这些按钮。就个人而言,我发现方向箭头更直观(默认情况下完***际化!)。
分页应该如何工作?
在对轮播进行分页时,我们可以采取两种明确的策略:逐项或 第一个隐藏项。只有一种正确的策略,但是因为我经常看到另一种策略的实施,所以我认为值得解释为什么 它是不正确的。
1. 逐项
只需测量列表中下一个项目的 x 偏移量,然后按该数量为架子设置动画。这是一个非常简单的算法,我认为选择它是因为它的简单性而不是用户友好性。
问题是大多数屏幕一次可以显示很多项目,人们会在尝试导航之前扫描所有项目。
感觉迟钝,如果不是完全令人沮丧的话。唯一可以选择的情况是,如果您 知道轮播中的项目宽度相同或仅略小于可视区域。
但是,如果我们正在查看多个项目,我们最好使用第一个模糊项目方法:
2. 第一个被隐藏的项目
这个方法只是在我们想要移动轮播的方向上 寻找第 一个被遮挡的项目,获取它的x 偏移量,然后滚动到那个位置。
在这样做的过程中,我们在假设用户已经看到当前存在的所有新项目的情况下提取了最大数量的新项目。
因为我们正在拉入更多项目,所以轮播需要更少的点击来导航。更快的导航将增加参与度并确保您的用户看到更多您的产品。
事件监听器
首先,让我们设置我们的事件监听器,这样我们就可以开始使用分页了。
我们首先需要选择上一个和下一个按钮。在函数顶部, carousel
添加:
const nextButton = container.queryselector('.next'); const prevButton = container.querySelector('.prev');
然后,在函数的 底部 ,carousel
添加事件***器:
nextButton.addEventListener('click', gotoNext); prevButton.addEventListener('click', gotoPrev);
最后,在您的事件***器块上方,添加实际函数:
function goto(delta) { } const gotoNext = () => goto(1); const gotoPrev = () => goto(-1);
goto
是处理所有分页逻辑的函数。它只需要一个代表我们希望分页的行进方向的数字。 gotoNext
并 简单地分别用or gotoPrev
调用这个函数 。1
-1
计算“页面”
用户可以自由滚动此轮播,其中有 n
项目,并且轮播可能会调整大小。所以传统页面的概念在这里并不直接适用。我们不会计算页数。
相反,当 goto
调用该函数时,我们将朝 的方向 delta
查找并找到第一个部分被遮挡的项目。这将成为我们下一个“页面”上的第一项。
第一步是获取我们滑块的当前 x 偏移量,并使用它与滑块的完整可见宽度来计算我们想要滚动到的“理想”偏移量。如果我们对滑块的内容很幼稚,理想的偏移量就是我们将滚动到的位置。它为我们提供了一个开始搜索我们的第一个项目的好地方。
const currentX = sliderX.get(); 让 targetX = currentX + (-sliderVisibleWidth * delta);
我们可以在这里使用厚颜无耻的优化。通过提供我们在上一个教程中创建targetX
的 clampXOffset
函数,我们可以查看它的输出是否与targetX
. 如果是,则意味着我们targetX
超出了可滚动范围,因此我们不需要找出最接近的项目。我们只是滚动到最后。
const clampedX = clampXOffset(targetX); targetX = (targetX === clampedX) ? findClosestItemOffset(targetX, delta) : clampedX;
查找最近的项目
请务必注意,以下代码的工作前提是 轮播中的所有项目都是相同的 size。在这种假设下,我们可以进行优化,例如不必测量每个项目的大小。如果您的物品 尺寸 不同,这仍然是一个很好的起点。
在您的函数上方 goto
,添加 findClosestItemOffset
最后一个片段中引用的函数:
function findClosestItem(targetX, delta) { }
首先,我们需要知道我们的项目有多宽以及它们之间的间距。该 Element.getBoundingClientRect()
方法可以提供我们需要的所有信息。对于 width
,我们只测量第一个 item 元素。要计算项目之间的间距,我们可以测量 right
第一项的left
偏移量和第二项的偏移量,然后从后者中减去前者:
const { right, width } = items[0].getBoundingClientRect(); const spacing = items[1].getBoundingClientRect().left - right;
现在,通过传递给函数的 targetX
和 delta
变量,我们拥有了快速计算要滚动到的偏移量所需的所有数据。
计算是用绝对值 targetX
除以 width + spacing
。这将为我们提供我们可以在该距离内放置的确切数量的项目。
const totalItems = Math.abs(targetX) / (width + spacing);
然后,根据分页方向(我们的 delta
)向上或向下舍入。这将为 我们提供可以容纳的完整项目的数量。
const totalCompleteItems = delta === 1 ? Math.floor(totalItems) : Math.ceil(totalItems);
最后,将该数字乘以 width + spacing
一个完整项目的偏移量。
return 0 - totalCompleteItems * (width + spacing);
动画分页
现在我们已经 targetX
计算好了,我们可以为它制作动画了!为此,我们将使用网络动画的主力, 补间。
对于外行来说,“补间”是补间的 缩写。补间在设定的持续时间内从一个值更改为另一个值。如果您使用过css转换,这也是同样的事情。
使用 javaScript 而不是 CSS 进行补间有许多好处(和缺点!)。在这种情况下,因为我们还 sliderX
使用物理和用户输入来制作动画,所以我们更容易留在补间的这个工作流程中。
这也意味着稍后我们可以连接一个进度条,它会自然地与我们所有的动画一起工作,而且是免费的。
我们首先tween
要从 Popmotion 导入:
const { calc, css, easing, physics, pointer, transform, tween, value } = window.popmotion;
在 goto
函数的最后,我们可以添加动画 from currentX
to 的补间targetX
:
tween({ from: currentX, to: targetX, onUpdate: sliderX }).start();
默认情况下,Popmotion 设置 duration
为 300
毫秒 ease
和 easing.easeOut
. 这些是专门挑选出来的,为响应用户输入的动画提供响应式感觉,但您可以随意玩耍,看看您是否想出了更适合您品牌感觉的东西。
进度指示器
对于用户来说,了解他们在轮播中的位置很有用。为此,我们可以连接一个进度指示器。
您的进度条可以通过多种方式设置样式。在本教程中,我们制作了一个 5px 高的彩色 div,它在上一个按钮和下一个按钮之间运行。这是我们将其连接到我们的代码并为栏设置动画的方式,这一点很重要,也是本教程的重点。
您还没有看到指示器,因为我们最初使用 transform: scaleX(0)
. 我们使用 scale
变换来调整条的宽度,因为正如我们在第 1 部分中解释的那样,变换比更改属性(如 left
在本例中为 width
.
它还允许我们轻松编写将比例设置为 百分比的代码:sliderX
介于minXOffset
和之间的当前值 maxXOffset
。
让我们从选择我们 div.progress-bar
的 previousButton
选择器开始:
const progressBar = container.querySelector('.progress-bar');
在我们定义之后 sliderRenderer
,我们可以添加一个渲染器 progressBar
:
const progressBarRenderer = css(progressBar);
现在让我们定义一个更新 scaleX
进度条的函数。
我们将使用一个 calc
名为 getProgressFromValue
. 这需要一个范围,在我们的例子中 min
和 maxXOffset
,以及第三个数字。它返回给定范围内第三个数字的进度,一个介于 0
和 之间的数字。1
function updateProgressBar(x) { const progress = calc.getProgressFromValue(maxXOffset, minXOffset, x); progressBarRenderer.set('scaleX', progress); }
我们在这里写的范围是 maxXOffset, minXOffset
什么时候,直观地,它应该被反转。这是因为 x
是一个负值, maxXOffset
也是一个负值,而 minXOffset
is 0
。从 0
技术上讲,这是两个数字中较大的一个,但较小的值实际上代表了最大偏移量。负数吧?
我们希望进度指示器与 同步更新 sliderX
,所以让我们更改这一行:
const sliderX = value(0, (x) => sliderRenderer.set('x', x));
到这一行:
const sliderX = value(0, (x) => { updateProgressBar(x); sliderRenderer.set('x', x); });
现在,每当 sliderX
更新时,进度条也会更新。
结论
这就是本期的内容!您可以在此CodePen上获取最新代码。我们成功地引入了水平滚轮滚动、分页和进度条。
到目前为止,旋转木马的状态非常好!在最后一期中,我们将更进一步。我们将使旋转木马完全可以通过键盘访问,以确保任何人都可以使用它。
- 关于限制滚动事件的旁白
- 分页应该如何工作?
- 1. 逐项
- 2. 第一个被隐藏的项目
- 事件监听器
- 计算“页面”
- 查找最近的项目
- 动画分页