理想のカルーセルを作る その1

王林
王林オリジナル
2023-09-04 18:25:09819ブラウズ

カルーセルはストリーミング Web サイトや電子商取引 Web サイトの定番です。 Amazon と Netflix はどちらも、重要なナビゲーション ツールとしてこれらを使用しています。このチュートリアルでは、両方のインタラクション デザインを評価し、その結果を使用して完璧なカルーセルを実装します。

このチュートリアル シリーズでは、JavaScript モーション エンジン Popmotion のいくつかの機能も学習します。トゥイーン (ページネーション用)、ポインタ トラッキング (スクロール用)、スプリング物理学 (楽しい仕上げ用) などのアニメーション ツールを提供します。

パート 1 では、Amazon と Netflix がスクロールを実装する方法を評価します。次に、タッチでスクロールできるカルーセルを実装します。

このシリーズの最後までに、スプリング物理を使用して、ホイールとタッチパッドのスクロール、ページング、プログレス バー、キーボード ナビゲーション、およびいくつかの小さなタッチを実装します。基本的な関数の構成についても触れます。

完璧ですか?

カルーセルを「完璧」にするにはどうすればよいですか?次の方法でアクセスできる必要があります:

  • マウス: クリックしやすく、コンテンツが見えにくくならない「前へ」ボタンと「次へ」ボタンを提供する必要があります。
  • タッチ: 指を追跡し、指が画面から離れたときと同じ勢いでスクロールする必要があります。
  • スクロール ホイール: 見落とされがちな Apple Magic Mouse と多くのラップトップ トラックパッドは、スムーズな水平スクロールを提供します。私たちはこれらの能力を活用する必要があります!
  • キーボード: 多くのユーザーは、マウスを使用して移動することを嫌がるか、使用できません。これらのユーザーも製品を使用できるように、カルーセルにアクセスできるようにすることが重要です。
最後に、さらに一歩進めて、スライダーが終点に達したときにカルーセルがスプリング物理学を介して明確かつ本能的に反応するようにし、自信を持って楽しいユーザー エクスペリエンスを実現します。

###設定###

まず、この CodePen をフォークして、基本的なカルーセルを構築するために必要な HTML と CSS を取得しましょう。

window.popmotion

からアクセスできる Popmotion も追加しました。

必要に応じて、コードをローカル プロジェクトにコピーできますが、環境が Sass と ES6 をサポートしていることを確認する必要があります。 npm install Popmotion

を使用して Popmotion をインストールする必要もあります。

新しいカルーセルを作成する

どのページにも、多数のカルーセルがある場合があります。したがって、それぞれの状態と機能をカプセル化する方法が必要です。

class

の代わりに

factory function を使用します。ファクトリ関数は、混乱を招きやすい this キーワードの使用を回避し、このチュートリアルのコードを簡素化します。 JavaScript エディターで、次の簡単な関数を追加します。 リーリー

この

carousel

関数にカルーセル固有のコードを追加します。

スクロールする方法とその理由

最初のタスクは、カルーセルをスクロールさせることです。この問題は 2 つの方法で解決できます:

ネイティブブラウザのスクロール

明白な解決策は、スライダーに

overflow-x:scroll

を設定することです。これにより、タッチ デバイスや水平マウス ホイール デバイスを含むすべてのブラウザでネイティブ スクロールが可能になります。

ただし、この方法には欠点もあります:

コンテナの外側のコンテンツは表示されないため、デザインが制限される可能性があります。

    また、アニメーションを使用して終点に到達したことを示す方法も制限されます。
  • デスクトップ ブラウザには、見苦しい (アクセス可能ではありますが!) 水平スクロールバーが表示されます。
  • ###または:###
  • アニメーション
  • 翻訳X

カルーセルの

translateX プロパティをアニメーション化することもできます。これは、好みのデザインを正確に実装できるため、非常に多用途になります。 translateX

も、CSS

left プロパティとは異なり、デバイスの GPU で処理できるため、非常に優れたパフォーマンスを発揮します。 欠点は、JavaScript を使用してスクロール機能を再実装する必要があることです。これは、より多くの作業とより多くのコードを意味します。 Amazon と Netflix はスクロールをどのように実装していますか?

Amazon と Netflix のカルーセルはどちらも、この問題を解決する上で異なるトレードオフを行っています。

Amazon

カルーセルの

left

プロパティをデスクトップ モードでアニメーション化します。

left をアニメーション化することは、これを変更するとレイアウトの再計算がトリガーされるため、非常に悪い選択です。これは CPU に負荷がかかるため、古いマシンでは 60fps に達するのに苦労します。 translateX の代わりに left をアニメーション化することに決めた人は、本当に馬鹿に違いありません (

スポイラー:

は 2012 年の私でした。当時、私たちはそれほど啓発されていませんでした) .) <p>当检测到触摸设备时,轮播会使用浏览器的本机滚动。仅在“移动”模式下启用此功能的问题是使用水平滚轮的桌面用户会错过。这也意味着轮播之外的任何内容都必须在视觉上被切断:</p> <p><img src="https://img.php.cn/upload/article/000/465/014/169382311682782.jpg" alt="制作理想的旋转木马,第 1 部分"></p> <p><strong>Netflix</strong> 正确动画轮播的 <code class="inline">translateX 属性,并且在所有设备上均如此。这使他们能够拥有一种在轮播之外渗透的设计:

制作理想的旋转木马,第 1 部分

这反过来又允许他们做出奇特的设计,其中项目在轮播的 x 和 y 边缘之外放大,而周围的项目移开:

制作理想的旋转木马,第 1 部分

不幸的是,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();

这里,fromvelocity 被设置为 sliderX 的当前值和速度。这确保了我们的物理模拟与用户的拖动运动具有相同的初始启动条件。​​

friction 被设置为 0.2。摩擦力设置为从 01 的值,其中 0 根本没有摩擦力,而 1 是绝对摩擦力摩擦。尝试使用此值来查看当用户停止拖动时它对轮播的“感觉”产生的变化。

数字越小,感觉越轻,数字越大,动作越重。对于滚动动作,我觉得 0.2 在不稳定和迟缓之间取得了很好的平衡。

边界

但是有一个问题!如果您一直在使用新的触摸旋转木马,那么这是显而易见的。我们没有限制移动,因此可以真正扔掉你的旋转木马!

还有另一个变压器可以完成这项工作,clamp。这也是一个柯里化函数,这意味着如果我们使用最小值和最大值调用它,例如 01,它将返回一个新函数。在此示例中,新函数将限制给定的任何数字在 01 之间:

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,我们可以从 foobar 组成一个新函数,我们可以重用它:

const foobar = pipe(foo, bar);
foobar(0);

它也是按照自然的开始 -> 结束顺序编写的,这使得它更容易理解。我们可以使用它将 applyOffsetclamp 组合成一个函数。导入 pipe

const { applyOffset, clamp, pipe } = transform;

output 回调替换为 pointer

pipe(
  ({ x }) => x,
  applyOffset(action.x.get(), sliderX.get()),
  clampXOffset,
  (v) => sliderX.set(v)
)

并将 physicalsoutput 回调替换为:

pipe(clampXOffset, (v) => sliderX.set(v))

这种函数组合可以非常巧妙地从较小的、可重用的函数中创建描述性的、逐步的流程。

现在,当您拖动并抛出轮播时,它不会移动到其边界之外。

突然停止并不是很令人满意。但这是稍后部分的问题!

结论

这就是第 1 部分的全部内容。到目前为止,我们已经研究了现有的轮播,以了解不同滚动方法的优点和缺点。我们使用 Popmotion 的输入跟踪和物理原理,通过触摸滚动为轮播的 translateX 提供高性能动画。我们还了解了函数组合和柯里化函数。

您可以在此 CodePen 上获取“到目前为止的故事”的评论版本。

在接下来的几期中,我们将介绍:

  • 用鼠标滚轮滚动
  • 当窗口大小调整时重新测量轮播
  • 分页,支持键盘和鼠标
  • 借助弹簧物理原理实现令人愉悦的触感

期待在那里见到您!

以上が理想のカルーセルを作る その1の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。