輪播是串流媒體和電子商務網站的主要內容。亞馬遜和 Netflix 都使用它們作為重要的導航工具。在本教程中,我們將評估兩者的互動設計,並利用我們的發現來實現完美的輪播。
在本教學系列中,我們也將學習 JavaScript 運動引擎 Popmotion 的一些功能。它提供了動畫工具,例如補間(用於分頁)、指針跟踪(用於滾動)和彈簧物理(用於我們令人愉悅的最後潤色。)
第 1 部分將評估 Amazon 和 Netflix 如何實現滾動。然後,我們將實現一個可以透過觸控滾動的輪播。
在本系列結束時,我們將使用彈簧物理實現滾輪和觸控板滾動、分頁、進度條、鍵盤導航和一些小觸控。我們還將接觸到一些基本的函數組合。
怎麼才能讓輪播變得「完美」?它必須可以透過以下方式存取:
最後,我們將更進一步,當滑塊到達末端時,讓輪播透過彈簧物理做出清晰而本能的回應,使其成為自信、令人愉悅的使用者體驗。
首先,讓我們透過分叉此 CodePen 來取得建立基本輪播所需的 HTML 和 CSS。
Pen 使用 Sass 來預處理 CSS,使用 Babel 來轉譯 ES6 JavaScript。我還添加了 Popmotion,可以透過 window.popmotion
存取它。
如果您願意,可以將程式碼複製到本機項目,但您需要確保您的環境支援 Sass 和 ES6。您還需要使用 npm install popmotion
安裝 Popmotion。
在任何給定頁面上,我們可能有許多輪播。所以我們需要一個方法來封裝每個的狀態和功能。
我將使用工廠函數而不是 class
。工廠函數避免了使用經常令人困惑的 this
關鍵字,並且將簡化本教程的程式碼。
在您的 JavaScript 編輯器中,加入這個簡單的函數:
function carousel(container) { } carousel(document.querySelector('.container'));
我們將在此 carousel
函數中新增特定於輪播的程式碼。
我們的第一個任務是使輪播滾動。我們可以透過兩種方法來解決這個問題:
顯而易見的解決方案是在滑桿上設定 overflow-x:scroll
。這將允許在所有瀏覽器上進行本機滾動,包括觸控和水平滑鼠滾輪設備。
但是,這種方法也有缺點:
或:
translateX
我們也可以為輪播的 translateX
屬性設定動畫。這將是非常通用的,因為我們能夠準確地實現我們喜歡的設計。 translateX
的效能也非常好,因為與 CSS left
屬性不同,它可以由裝置的 GPU 處理。
缺點是,我們必須使用 JavaScript 重新實作滾動功能。這意味著更多的工作、更多的程式碼。
亞馬遜和 Netflix 輪播在解決這個問題時都做出了不同的權衡。
Amazon 在「桌面」模式下為輪播的 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 };
我们现在可以向 document
添加一个 touchmove
事件监听器,以根据此 touchOrigin
确定拖动方向:
document.addEventListener('touchmove', determineDragDirection);
我们的 defineDragDirection
函数将测量下一个触摸位置,检查它是否实际移动,如果是,则测量角度以确定它是垂直还是水平:
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); }
如果提供的角度在 -90 +/- 45 度(垂直向上)或 90 +/-45 度(垂直向下)范围内,此函数将返回 true
。因此我们可以添加另一个 return
子句,如果此函数返回 true
。
if (angleIsVertical(angle)) return;
现在我们知道用户正在尝试滚动轮播,我们可以开始跟踪他们的手指。 Popmotion 提供了一个指针操作,可输出鼠标或触摸指针的 x/y 坐标。
首先导入指针
:
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
和 1
,它将返回一个新函数。在此示例中,新函数将限制给定的任何数字在 0
和 1
之间:
clamp(0, 1)(5); // returns 1
首先导入clamp
:
const { applyOffset, clamp } = transform;
我们希望在轮播中使用此夹紧功能,因此在定义 minXOffset
后添加此行:
const clampXOffset = clamp(minXOffset, maxXOffset);
我们将使用 pipe
转换器使用一些轻量函数组合来修改我们在操作上设置的两个 output
。
当我们调用函数时,我们这样写:
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) )
并将 physicals
的 output
回调替换为:
pipe(clampXOffset, (v) => sliderX.set(v))
这种函数组合可以非常巧妙地从较小的、可重用的函数中创建描述性的、逐步的流程。
现在,当您拖动并抛出轮播时,它不会移动到其边界之外。
突然停止并不是很令人满意。但这是稍后部分的问题!
这就是第 1 部分的全部内容。到目前为止,我们已经研究了现有的轮播,以了解不同滚动方法的优点和缺点。我们使用 Popmotion 的输入跟踪和物理原理,通过触摸滚动为轮播的 translateX
提供高性能动画。我们还了解了函数组合和柯里化函数。
您可以在此 CodePen 上获取“到目前为止的故事”的评论版本。
在接下来的几期中,我们将介绍:
期待在那里见到您!
以上是製作理想的旋轉木馬,第 1 部分的詳細內容。更多資訊請關注PHP中文網其他相關文章!