簡単なスクロールボックスの実装


これは、モバイルとデスクトップのUIで非常に一般的なコンポーネントです.便利なときに水平方向のリストを表示します.下のイメージは、色のリストを表示するスクロールボックスの例を示します.そして、それは我々が反応で再生されるつもりであるので、あなたはあなたが望む何かを表示するためにあなたのプロジェクトにそれを適用することができます!
すべてのコードは、このgitリポジトリにありますhere .

コーディングを始めましょう


基本構造


私たちのスクロールボックスは、水平スクロールを持つラッパーとそのコンテンツ幅を持つコンテナから成ります.
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import './scrollBox.css';

function ScrollBox({ children }) {
  return (
    <div className="scroll-box">
      <div className="scroll-box__wrapper">
        <div className="scroll-box__container" role="list">
          {children.map((child, i) => (
            <div className="scroll-box__item" role="listitem" key={`scroll-box-item-${i}`}>
              {child}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

ScrollBox.propTypes = {
  children: PropTypes.node.isRequired,
};

export default ScrollBox;
スタイルは、ラッパーが水平スクロールを作成し、コンテナーがその内容をインラインで表示することを保証する必要があります.
.scroll-box {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.scroll-box__wrapper {
  width: 100%;
  height: 100%;
  overflow-y: hidden;
  overflow-x: scroll;
}

.scroll-box__container {
  height: 100%;
  display: inline-flex;
}

スクロールバーの削除


携帯電話とデスクトップのバージョンがまだスクロールバーを表示することがわかりました、そして、それは我々が必要とするものでないかもしれません.CSSを使用するので、それを隠すことが可能です.CSSファイルは以下のようになります.
.scroll-box {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.scroll-box__wrapper {
  width: 100%;
  height: 100%;
  overflow-y: hidden;
  overflow-x: scroll;
+   -ms-overflow-style: none; /* IE */
+   overflow: -moz-scrollbars-none; /* Firefox */
}

+ .scroll-box__wrapper::-webkit-scrollbar {
+   display: none; /* Chrome and Safari */
+ }

.scroll-box__container {
  height: 100%;
  display: inline-flex;
}

今度はスクロールバーが消えます.あなたがモバイルUIのためのこのコンポーネントをしたい場合は、それは行く準備ができている!あなたはすでに画面タッチで非常に素晴らしいスクロール動作があります.しかし、あなたがそれを必要とするならば、マウスポインタでスクロールしているデスクトップ・ブラウザーで使われるために、次の線を読んでください.
ここでは、我々はそう簡単な部分を開始します.

マウスポインターでスクロールを制御する


まず第一に、私たちはref 我々のラッパーのように我々はイベントに機能を取り付けることができますonmousemove , onmousedown , onmouseup , and onmouseleave . それでフックを使いましょうuseRef 作成するscrollWrapperRef そして、それを我々のラッパー部門に渡してください.
次のステップは、refを設定したときに上記のイベントに関数をアタッチすることです.コードは次のようになります.
import React, { useRef, useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import './scrollBox.css';

function ScrollBox({ children }) {
  const scrollWrapperRef = useRef();

  const scrollWrapperCurrent = scrollWrapperRef.current;
  useEffect(() => {
    if (scrollWrapperRef.current) {
      const handleDragStart = () => {};
      const handleDragMove = () => {};
      const handleDragEnd = () => {};

      if (scrollWrapperRef.current.ontouchstart === undefined) {
        scrollWrapperRef.current.onmousedown = handleDragStart;
        scrollWrapperRef.current.onmousemove = handleDragMove;
        scrollWrapperRef.current.onmouseup = handleDragEnd;
        scrollWrapperRef.current.onmouseleave = handleDragEnd;
      }
    }
  }, [scrollWrapperCurrent]);

  return (
    <div className="scroll-box">
      <div className="scroll-box__wrapper" ref={scrollWrapperRef}>
        <div className="scroll-box__container" role="list">
          {children.map((child, i) => (
            <div role="listitem" key={`scroll-box-item-${i}`} className="scroll-box__item">
              {child}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

ScrollBox.propTypes = {
  children: PropTypes.node.isRequired,
};

export default ScrollBox;

ハンドル
マウスボタンを押すと、ドラッグが開始したことを理解し、X軸と現在のスクロール位置に初期ポインタ位置を保存する必要があります.それが我々のやりようだ.
...
const [clickStartX, setClickStartX] = useState();
const [scrollStartX, setScrollStartX] = useState();
...
const handleDragStart = e => {
  setClickStartX(e.screenX);
  setScrollStartX(scrollWrapperRef.current.scrollLeft);
};

握手
マウスボタンを押してカーソルを移動させながら、スクロールがドラッグされていることを理解しているので、マウスのX軸のデルタを初期水平スクロールに加えて、ラッパ水平スクロールに設定します.これは、マウスポインタの位置に従います.
...
const [clickStartX, setClickStartX] = useState();
const [scrollStartX, setScrollStartX] = useState();
...
const handleDragMove = e => {
  if (clickStartX !== undefined && scrollStartX !== undefined) {
    const touchDelta = clickStartX - e.screenX;
    scrollWrapperRef.current.scrollLeft = scrollStartX + touchDelta;
  }
};

手淫
マウスボタンを離すか、スクロールボックス領域を残すことは、ドラッグを止めることとして理解されます.そして、そのために、我々はちょうどClickStarTXとScrollStartXをunsetしたいので、handleDragmoveはもうスクロール・レフトをセットしません.
...
const [clickStartX, setClickStartX] = useState();
const [scrollStartX, setScrollStartX] = useState();
...
const handleDragEnd = () => {
  if (clickStartX !== undefined) {
    setClickStartX(undefined);
    setScrollStartX(undefined);
  }
};

UseEffectsの中でマウスイベントを設定する理由は?


あなたはなぜ私たちがuseeffectの中でそれをセットする必要があるかについて、あなた自身に尋ねているかもしれません.主な理由は、マウスイベントをトリガーにすることですscrollWrapperRef.current , でも一度scrollWrapperRef 変更可能なオブジェクトですscrollWrapperRef.current それはconstにscrollWrapperCurrent . それは、その効果を理解するために有効になりますcurrent 内部scrollWrapperRef 変更.

デスクトップブラウザのマウス位置の追跡のみ


モバイルブラウザについてscrollWrapperRef.current.ontouchstartnull 使用することができますが、設定されていないことを意味します.デスクトップブラウザーでは、値が未定義です、一度我々がスクリーン(少なくともほとんどのコンピュータで)に『タッチ』を持たないならば.それで、我々はちょうどそれがデスクトップブラウザーで起こることを望みます.
私はデスクトップタッチスクリーンでそれをテストするチャンスを持っていなかった.あなたがそうするならば、コメントを残してください!
  if (scrollWrapperRef.current.ontouchstart === undefined) {
    scrollWrapperRef.current.onmousedown = handleDragStart;
    scrollWrapperRef.current.onmousemove = handleDragMove;
    scrollWrapperRef.current.onmouseup = handleDragEnd;
    scrollWrapperRef.current.onmouseleave = handleDragEnd;
  }

物理学を加えましょう!


あなたが見ることができるように、移動は、ポインタがドラッグを停止し、同じ場所で停止し、それは我々が携帯電話の経験で得られないものです.そのために、我々は運動量効果を加えなければなりません.それはその速度を維持し、穏やかに遅くする必要があります.
handleDragMove 我々はマウスの移動速度をキャプチャする必要があります.それをするために、我々は速度方程式を使いますv = ds/dt , または時間間隔で空間の変化.もう少し明確にするために以下のコードを見てください.
const timing = (1 / 60) * 1000;
...
const [isDragging, setIsDragging] = useState(false);
const [lastScreenX, setLastScreenX] = useState(0);
const [speed, setSpeed] = useState(0);
const [direction, setDirection] = useState(0);

const handleLastScrollX = useCallback(
  throttle(screenX => {
    setLastScreenX(screenX);
  }, timing),
  []
);
...
const handleDragMove = e => {
  if (clickStartX !== undefined && scrollStartX !== undefined) {
    const touchDelta = clickStartX - e.screenX;
    scrollWrapperRef.current.scrollLeft = scrollStartX + touchDelta;

    if (Math.abs(touchDelta) > 1) {
      setIsDragging(true);
      setDirection(touchDelta / Math.abs(touchDelta));
      setSpeed(Math.abs((lastScreenX - e.screenX) / timing));
      setLastScreenX(e.screenX);
    }
  }
};
からlodash SetlastScrollXを16.16.6667 msの1回だけ設定することを保証するスロットル関数を取得します.(1 / 60) * 1000 , 何がブラウザから2番目の画面の更新60フレームに一致します.
The (lastScreenX - e.screenX) / timing マウスポインタの現在の速度を与えます.And touchDelta / Math.abs(touchDelta) 移動方向のヒントとしてO - 1または1の結果を提供します.
スクロールボックスをドラッグした後の移動の継続を適用するには、以下のように有効に使用できます.
const timing = (1 / 60) * 1000;
const decay = v => -0.1 * ((1 / timing) ^ 4) + v;
...
const [momentum, setMomentum] = useState(0);
...
const handleMomentum = useCallback(
  throttle(nextMomentum => {
    setMomentum(nextMomentum);
    scrollRef.current.scrollLeft = scrollRef.current.scrollLeft + nextMomentum * timing * direction;
  }, timing),
  [scrollWrapperCurrent, direction]
);

useEffect(() => {
  if (direction !== 0) {
    if (momentum > 0 && !isDragging) {
      handleMomentum(decay(momentum));
    } else if (isDragging) {
      setMomentum(speed);
    } else {
      setDirection(0);
    }
  }
}, [momentum, isDragging, speed, direction, handleMomentum]);
減衰関数は、速度と時間の値の指数関数的減少を記述する.ちょうど我々が必要!それで、isdraggingが我々のhandledragendの上でfalseにセットされたあと、それがゼロに達するまで、それは毎回再計算される運動量に起因する転位の価値を加えるようになります.
スクロールボックスをクリックした後に移動を停止するには、方向をゼロに設定します.
const handleDragStart = e => {
  ...
  setDirection(0);
};

リンクと画像のドラッグ


既に追跡しているドラッグを使用して、コンテナポインタイベントをnone . だからリンク、ボタンや画像をドラッグしながら、それが正常にドラッグされるように行動するつもりです.
const handleDragMove = e => {
  e.preventDefault();
  e.stopPropagation();
  ...
}
...
return (
  <div className="scroll-box">
    <div className="scroll-box__wrapper" ref={scrollWrapperRef}>
      <div className="scroll-box__container" role="list" style={{ pointerEvents: isDragging ? 'none' : undefined }}>
        {children.map((child, i) => (
          <div role="listitem" key={`scroll-box-item-${i}`} className="scroll-box__item">
            {child}
          </div>
        ))}
      </div>
    </div>
  </div>
 );

最終成分


import React, { useRef, useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import throttle from 'lodash/throttle';
import './scrollBox.css';

const timing = (1 / 60) * 1000;
const decay = v => -0.1 * ((1 / timing) ^ 4) + v;

function ScrollBox({ children }) {
  const scrollWrapperRef = useRef();
  const [clickStartX, setClickStartX] = useState();
  const [scrollStartX, setScrollStartX] = useState();
  const [isDragging, setIsDragging] = useState(false);
  const [direction, setDirection] = useState(0);
  const [momentum, setMomentum] = useState(0);
  const [lastScrollX, setLastScrollX] = useState(0);
  const [speed, setSpeed] = useState(0);
  const handleLastScrollX = useCallback(
    throttle(screenX => {
      setLastScrollX(screenX);
    }, timing),
    []
  );
  const handleMomentum = useCallback(
    throttle(nextMomentum => {
      setMomentum(nextMomentum);
      scrollRef.current.scrollLeft = scrollRef.current.scrollLeft + nextMomentum * timing * direction;
    }, timing),
    [scrollWrapperCurrent, direction]
  );
  useEffect(() => {
    if (direction !== 0) {
      if (momentum > 0.1 && !isDragging) {
        handleMomentum(decay(momentum));
      } else if (isDragging) {
        setMomentum(speed);
      } else {
        setDirection(0);
      }
    }
  }, [momentum, isDragging, speed, direction, handleMomentum]);

  const scrollWrapperCurrent = scrollWrapperRef.current;
  useEffect(() => {
    if (scrollWrapperRef.current) {
      const handleDragStart = e => {
        setClickStartX(e.screenX);
        setScrollStartX(scrollWrapperRef.current.scrollLeft);
        setDirection(0);
      };
      const handleDragMove = e => {
        e.preventDefault();
        e.stopPropagation();

        if (clickStartX !== undefined && scrollStartX !== undefined) {
          const touchDelta = clickStartX - e.screenX;
          scrollWrapperRef.current.scrollLeft = scrollStartX + touchDelta;

          if (Math.abs(touchDelta) > 1) {
            setIsDragging(true);
            setDirection(touchDelta / Math.abs(touchDelta));
            setSpeed(Math.abs((lastScrollX - e.screenX) / timing));
            handleLastScrollX(e.screenX);
          }
        }
      };
      const handleDragEnd = () => {
        if (isDragging && clickStartX !== undefined) {
          setClickStartX(undefined);
          setScrollStartX(undefined);
          setIsDragging(false);
        }
      };

      if (scrollWrapperRef.current.ontouchstart === undefined) {
        scrollWrapperRef.current.onmousedown = handleDragStart;
        scrollWrapperRef.current.onmousemove = handleDragMove;
        scrollWrapperRef.current.onmouseup = handleDragEnd;
        scrollWrapperRef.current.onmouseleave = handleDragEnd;
      }
    }
  }, [scrollWrapperCurrent, clickStartX, isDragging, scrollStartX, handleLastScrollX, lastScrollX]);

  return (
    <div className="scroll-box">
      <div className="scroll-box__wrapper" ref={scrollWrapperRef}>
        <div className="scroll-box__container" role="list" style={{ pointerEvents: isDragging ? 'none' : undefined }}>
          {children.map((child, i) => (
            <div role="listitem" key={`scroll-box-item-${i}`} className="scroll-box__item">
              {child}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

ScrollBox.propTypes = {
  children: PropTypes.node.isRequired,
};

export default ScrollBox;

改善!


我々はフックを作成することにより、我々のコンポーネントからすべてのロジックを削除するフックを使用することができます!そして、それは死んだ簡単!
フックはusesrollboxと呼ばれます.
import { useState, useEffect, useCallback } from 'react';
import throttle from 'lodash/throttle';

const timing = (1 / 60) * 1000;
const decay = v => -0.1 * ((1 / timing) ^ 4) + v;

function useScrollBox(scrollRef) {
  const [clickStartX, setClickStartX] = useState();
  const [scrollStartX, setScrollStartX] = useState();
  const [isDragging, setIsDragging] = useState(false);
  const [direction, setDirection] = useState(0);
  const [momentum, setMomentum] = useState(0);
  const [lastScrollX, setLastScrollX] = useState(0);
  const [speed, setSpeed] = useState(0);

  const scrollWrapperCurrent = scrollRef.current;
  const handleLastScrollX = useCallback(
    throttle(screenX => {
      setLastScrollX(screenX);
    }, timing),
    []
  );
  const handleMomentum = useCallback(
    throttle(nextMomentum => {
      setMomentum(nextMomentum);
      scrollRef.current.scrollLeft = scrollRef.current.scrollLeft + nextMomentum * timing * direction;
    }, timing),
    [scrollWrapperCurrent, direction]
  );
  useEffect(() => {
    if (direction !== 0) {
      if (momentum > 0.1 && !isDragging) {
        handleMomentum(decay(momentum));
      } else if (isDragging) {
        setMomentum(speed);
      } else {
        setDirection(0);
      }
    }
  }, [momentum, isDragging, speed, direction, handleMomentum]);

  useEffect(() => {
    if (scrollRef.current) {
      const handleDragStart = e => {
        setClickStartX(e.screenX);
        setScrollStartX(scrollRef.current.scrollLeft);
        setDirection(0);
      };
      const handleDragMove = e => {
        e.preventDefault();
        e.stopPropagation();

        if (clickStartX !== undefined && scrollStartX !== undefined) {
          const touchDelta = clickStartX - e.screenX;
          scrollRef.current.scrollLeft = scrollStartX + touchDelta;

          if (Math.abs(touchDelta) > 1) {
            setIsDragging(true);
            setDirection(touchDelta / Math.abs(touchDelta));
            setSpeed(Math.abs((lastScrollX - e.screenX) / timing));
            handleLastScrollX(e.screenX);
          }
        }
      };
      const handleDragEnd = () => {
        if (isDragging && clickStartX !== undefined) {
          setClickStartX(undefined);
          setScrollStartX(undefined);
          setIsDragging(false);
        }
      };

      if (scrollRef.current.ontouchstart === undefined) {
        scrollRef.current.onmousedown = handleDragStart;
        scrollRef.current.onmousemove = handleDragMove;
        scrollRef.current.onmouseup = handleDragEnd;
        scrollRef.current.onmouseleave = handleDragEnd;
      }
    }
  }, [scrollWrapperCurrent, clickStartX, isDragging, scrollStartX, handleLastScrollX, lastScrollX]);

  return { clickStartX, scrollStartX, isDragging, direction, momentum, lastScrollX, speed };
}

export default useScrollBox;
そして、私たちのコンポーネントは、他のフックとしてそれを使用することができます.
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import useScrollBox from './useScrollBox';
import './scrollBox.css';

function ScrollBox({ children }) {
  const scrollWrapperRef = useRef();
  const { isDragging } = useScrollBox(scrollWrapperRef);
  return (
    <div className="scroll-box">
      <div className="scroll-box__wrapper" ref={scrollWrapperRef}>
        <div className="scroll-box__container" role="list" style={{ pointerEvents: isDragging ? 'none' : undefined }}>
          {children.map((child, i) => (
            <div role="listitem" key={`scroll-box-item-${i}`} className="scroll-box__item">
              {child}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

ScrollBox.propTypes = {
  children: PropTypes.node.isRequired,
};

export default ScrollBox;
今どのくらいきれい?希望を楽しんでいると何か新しいことを学んだ!