在本教程系列中,我们还将学习 javascript运动引擎Popmotion的一些功能。它提供了动画工具,如补间(用于分页)、指针跟踪(用于滚动)和弹簧物理(用于我们令人愉快的收尾工作。)
第 1 部分将评估 Amazon 和 Netflix 如何实现滚动。然后我们将实现一个可以通过触摸滚动的轮播。
在本系列结束时,我们将使用弹簧物理实现滚轮和触摸板滚动、分页、进度条、键盘导航和一些小触摸。我们还将接触到一些基本的功能组合。
完美的?
旋转木马需要什么才能“完美”?它必须可以通过以下方式访问:
鼠标:它应该提供易于点击且不会遮挡内容的上一个和下一个按钮。
触摸: 它应该跟踪手指,然后以与手指离开屏幕时相同的动量滚动。
滚轮: 经常被忽视,Apple Magic Mouse 和许多笔记本电脑触控板提供流畅的水平滚动。我们应该利用这些能力!
键盘:许多用户不喜欢或无法使用鼠标进行导航。重要的是,我们让轮播可访问,以便这些用户也可以使用我们的产品。
最后,我们将更进一步,通过让旋转木马在滑块到达末端时通过弹簧物理做出清晰而发自内心的响应,从而使其成为一个自信、令人愉快的 UX。
设置
首先,让我们通过 fork这个 CodePen来获取构建基本轮播所需 的 html 和css。
Pen 设置了用于预处理 CSS 的 Sass 和用于转译 ES6 JavaScript 的 Babel。我还包括了 Popmotion,可以使用window.popmotion
.
如果您愿意,可以将代码复制到本地项目,但您需要确保您的环境支持 Sass 和 ES6。您还需要使用npm install popmotion
.
创建一个新的轮播
在任何给定的页面上,我们可能有很多轮播。所以我们需要一个方法来封装每个的状态和功能。
我将使用 工厂函数而不是class
. 工厂函数避免了使用经常令人困惑的this
关键字的需要,并将为本教程的目的简化代码。
在你的 javaScript 编辑器中,添加这个简单的函数:
function carousel(container) { } carousel(document.queryselector('.container'));
我们将在此 carousel
函数中添加特定于轮播的代码。
滚动的方式和原因
我们的第一个任务是制作轮播滚动。我们有两种方法可以解决这个问题:
本机浏览器滚动
显而易见的解决方案是overflow-x: scroll
在滑块上设置。这将允许在所有浏览器上进行本机滚动,包括触摸和水平鼠标滚轮设备。
但是,这种方法也有缺点:
容器外的内容是不可见的,这对我们的设计来说是有限的。
它还限制了我们使用动画来表示我们已经到达终点的方式。
桌面浏览器会有一个丑陋的(虽然可以访问!)水平滚动条。
或者:
动画translateX
我们还可以为轮播的translateX
属性设置动画。这将是非常通用的,因为我们能够准确地实现我们喜欢的设计。translateX
性能也非常好,与 CSSleft
属性不同,它可以由设备的 GPU 处理。
不利的一面是,我们必须使用 JavaScript 重新实现滚动功能。那是更多的工作,更多的代码。
亚马逊和 Netflix 如何实现滚动?
亚马逊和 Netflix 轮播在解决这个问题时做出了不同的权衡。
亚马逊left
在“桌面”模式下为轮播的属性设置动画。动画 left
是一个非常糟糕的选择,因为更改它 会触发布局重新计算。这是 CPU 密集型的,较旧的机器将难以达到 60fps 。
决定用动画 left
代替动画的人translateX
一定是个真正的白痴(剧透:是我,早在 2012 年。那时我们还没有那么开明。)
当它检测到触摸设备时,轮播会使用浏览器的原生滚动。仅在“移动”模式下启用此功能的问题是缺少水平滚轮的桌面用户。这也意味着轮播之外的任何内容都必须在视觉上被切断:
Netflix正确地为轮播的translateX
属性设置动画,并且在所有设备上都这样做。这使他们能够拥有在旋转木马外流血的设计:
反过来,这使他们能够做出奇特的设计,其中项目被放大到轮播的 x 和 y 边缘之外,并且周围的项目移开:
不幸的是,Netflix 对触摸设备滚动的重新实现并不令人满意:它使用基于手势的分页系统,感觉缓慢且繁琐。也没有考虑水平滚轮。
我们可以做得更好。让我们编码吧!
像专业人士一样滚动
我们的第一步是抢.slider
节点。当我们这样做的时候,让我们抓住它包含的项目,这样我们就可以计算出滑块的尺寸。
function carousel(container) { const slider = container.querySelector('.slider'); const items = slider.querySelectorAll('.item'); }
测量旋转木马
我们可以通过测量滑块的宽度来确定滑块的可见区域:
const sliderVisibleWidth = slider.offsetWidth;
我们还需要其中包含的所有项目的总宽度。为了保持我们的 carousel
函数相对干净,让我们把这个计算放在文件顶部的一个单独的函数中。
通过使用getBoundingClientRect
测量left
我们的第一项的right
偏移量和我们的最后一项的偏移量,我们可以使用它们之间的差异来找到所有项的总宽度。
function getTotalItemsWidth(items) { const { left } = items[0].getBoundingClientRect(); const { right } = items[items.length - 1].getBoundingClientRect(); return right - left; }
在我们sliderVisibleWidth
测量之后,写下:
const totalItemsWidth = getTotalItemsWidth(items);
我们现在可以计算出我们的轮播应该允许滚动的最大距离。它是我们所有项目的总宽度, 减去可见滑块的一个完整宽度。这提供了一个数字,允许最右边的项目与我们滑块的右侧对齐:
const maxXOffset = 0; const minXOffset = - (totalItemsWidth - sliderVisibleWidth);
有了这些测量,我们就可以开始滚动我们的轮播了。
环境 translateX
Popmotion 带有一个 CSS 渲染器 ,用于简单且高效地设置 CSS 属性。它还带有一个值 函数,可用于跟踪数字,重要的是(我们很快就会看到)查询它们的速度。
在 JavaScript 文件的顶部,像这样导入它们:
const { css, value } = window.popmotion;
然后,在我们设置后的那一行 minXOffset
,为我们的滑块创建一个 CSS 渲染器:
const sliderRenderer = css(slider);
并创建一个value
来跟踪我们滑块的 x 偏移量并在滑块translateX
更改时更新它的属性:
const sliderX = value(0, (x) => sliderRenderer.set('x', x));
现在,水平移动滑块就像编写一样简单:
sliderX.set(-100);
试试看!
触摸滚动
我们希望我们的轮播开始滚动时 用户 水平拖动滑块并在用户停止触摸屏幕时停止滚动。我们的事件处理程序将如下所示:
let action; function stopTouchScroll() { document.removeeventListener('touchend', stopTouchScroll); } function startTouchScroll(e) { document.addEventListener('touchend', stopTouchScroll); } slider.addEventListener('touchstart', startTouchScroll, { passive: false });
在我们的startTouchScroll
函数中,我们想要:
停止任何其他通电动作
sliderX
。找到原点接触点。
监听下一个
touchmove
事件,看看用户是垂直拖动还是水平拖动。
之后document.addEventListener
,添加:
if (action) action.stop();
这将阻止任何其他动作(例如我们将在 中实现的物理驱动的动量滚动stopTouchScroll
)移动滑块。如果滑块滚动到他们想要单击的项目或标题,这将允许用户立即“抓住”滑块。
接下来,我们需要存储原点接触点。这将使我们能够看到用户接下来将手指移动到哪里。如果是垂直移动,我们将允许像往常一样滚动页面。如果是水平移动,我们将滚动滑块。
我们希望touchOrigin
在事件处理程序之间共享它。所以let action;
添加后:
let touchOrigin = {};
回到我们的startTouchScroll
处理程序,添加:
const touch = e.touches[0]; touchOrigin = { x: touch.pageX, y: touch.pageY };
我们现在可以添加一个touchmove
事件监听器来根据这个document
来确定拖动方向touchOrigin
:
document.addEventListener('touchmove', determineDragDirection);
我们的determineDragDirection
函数将测量下一个触摸位置,检查它是否实际移动,如果是,则测量角度以确定它是垂直还是水平:
function determineDragDirection(e) { const touch = e.changedTouches[0]; const touchLocation = { x: touch.pageX, y: touch.pageY }; }
Popmotion 包含一些有用的计算器,用于测量两个 x/y 坐标之间的距离等。我们可以像这样导入:
const { calc, css, value } = window.popmotion;
然后测量两点之间的距离是使用distance
计算器的问题:
const distance = calc.distance(touchOrigin, touchLocation);
现在如果触摸已经移动,我们可以取消设置这个事件监听器。
if (!distance) return; document.removeEventListener('touchmove', determineDragDirection);
angle
用计算器测量两点之间的角度:
const angle = calc.angle(touchOrigin, touchLocation);
我们可以通过将其传递给以下函数来使用它来确定该角度是水平角还是垂直角。将此函数添加到我们文件的最顶部:
function angleIsVertical(angle) { const isUp = ( angle <= -90 + 45 && angle >= -90 - 45 ); const isDown = ( angle <= 90 + 45 && angle >= 90 - 45 ); return (isUp || isDown); }
true
如果提供的角度在 -90 +/- 45 度(垂直向上)或 90 +/-45 度(垂直向下)范围内,则此函数返回。因此,如果此函数返回,我们可以添加另一个return
子句true
。
if (angleIsVertical(angle)) return;
指针跟踪
现在我们知道用户正在尝试滚动轮播,我们可以开始跟踪他们的手指。Popmotion 提供了一个 指针 动作,它将输出鼠标或触摸指针的 x/y 坐标。
首先,导入pointer
:
const { calc, css, pointer, value } = window.popmotion;
要跟踪触摸输入,请将原始事件提供给pointer
:
action = pointer(e).start();
我们想要测量 x
指针的初始位置并将任何移动应用到滑块。为此,我们可以使用一个 名为applyOffset
.
转换器是纯函数,它接受一个值,并返回它——是的——转换。例如:const double = (v) => v * 2
。
const { calc, css, pointer, transform, value } = window.popmotion; const { applyOffset } = transform;
applyOffset
是一个柯里化函数。这意味着当我们调用它时,它会创建一个新函数,然后可以传递一个值。我们首先用一个我们想要测量偏移量的数字来调用它,在这种情况下是 的当前值 action.x
,以及一个应用该偏移量的数字。在这种情况下,这就是我们的sliderX
.
所以我们的 applyOffset
函数将如下所示:
const applyPointerMovement = applyOffset(action.x.get(), sliderX.get());
我们现在可以在指针的output
回调中使用这个函数来将指针移动应用到滑块上。
action.output(({ x }) => slider.set(applyPointerMovement(x)));
停止,有风格
旋转木马现在可以通过触摸拖动!您可以使用 chrome 的开发者工具中的设备模拟来测试这一点。
感觉有点笨拙,对吧?您之前可能遇到过这样的滚动感觉:您抬起手指,滚动就停止了。或者滚动停止,然后一个小动画接管以假装滚动的继续。
我们不会那样做。我们可以使用 Popmotion 中的物理动作来获取它的真实速度sliderX
并在一段时间内对其施加摩擦。
首先,将其添加到我们不断增长的导入列表中:
const { calc, css, physics, pointer, value } = window.popmotion;
然后,在我们stopTouchScroll
函数的最后,添加:
if (action) action.stop(); action = physics({ from: sliderX.get(), velocity: sliderX.getVelocity(), friction: 0.2 }) .output((v) => sliderX.set(v)) .start();
在这里,from
和velocity
被设置为 的当前值和速度sliderX
。这确保我们的物理模拟具有与用户拖动动作相同的初始启动条件。
friction
被设置为 0.2
。摩擦力设置为从 0
到 的值1
, 0
完全没有摩擦力并且 1
是绝对摩擦力。尝试使用此值来查看当用户停止拖动时它对轮播的“感觉”所做的更改。
较小的数字会使它感觉更轻,而较大的数字会使运动更重。对于滚动动作,我觉得0.2
在不稳定和缓慢之间取得了很好的平衡。
边界
但是有问题!如果您一直在玩新的触控轮播,那是显而易见的。我们没有限制运动,因此可以从字面上扔掉您的旋转木马!
这项工作还有另一个变压器, clamp
. 这也是一个柯里化函数,这意味着如果我们用最小值和最大值调用它,比如 0
and 1
,它将返回一个新函数。在这个例子中,新函数会将给它的任何数字限制在 0
和 之间1
:
clamp(0, 1)(5); // returns 1
首先,导入clamp
:
const { applyOffset, clamp } = transform;
我们想在我们的轮播中使用这个钳位函数,所以在我们定义之后添加这一行 minXOffset
:
const clampXOffset = clamp(minXOffset, maxXOffset);
我们将 使用变压器output
的一些轻功能组合来修改我们在动作中设置的两个。pipe
管道
当我们调用一个函数时,我们这样写:
foo(0);
如果我们想将该函数的输出提供给另一个函数,我们可以这样写:
bar(foo(0));
这变得有点难以阅读,而且随着我们添加越来越多的功能,它只会变得更糟。
有了 pipe
,我们可以组成一个新的函数 foo
, bar
我们可以重用它:
const foobar = pipe(foo, bar);foobar(0);
它也是以自然的开始 -> 结束顺序编写的,这样更容易理解。我们可以使用它来组合 applyOffset
成 clamp
一个函数。进口 pipe
:
const { applyOffset, clamp, pipe } = transform;
将我们的output
回调 替换pointer
为:
pipe( ({ x }) => x, applyOffset(action.x.get(), sliderX.get()), clampXOffset, (v) => sliderX.set(v) )
output
并将回调 替换为physics
:
pipe(clampXOffset, (v) => sliderX.set(v))
这种功能组合可以非常巧妙地从更小的、可重用的功能中创建描述性的、逐步的过程。
现在,当您拖动并扔出旋转木马时,它不会超出其边界。
突然停止不是很令人满意。但这是后面部分的问题!
结论
这就是第 1 部分的全部内容。到目前为止,我们已经查看了现有的轮播,以了解不同滚动方法的优缺点。我们使用了 Popmotion 的输入跟踪和物理特性 translateX
,通过触摸滚动来高效地为我们的轮播设置动画。我们还介绍了函数组合和柯里化函数。
- 创建一个新的轮播
- 本机浏览器滚动
- 动画translateX
- 亚马逊和 Netflix 如何实现滚动?
- 测量旋转木马
- 环境 translateX
- 触摸滚动
- 指针跟踪
- 停止,有风格
- 边界
- 管道