Анимация перетаскивания элементов списка реагирования

Фрагмент ниже содержит массив из 10 элементов. Я могу перетаскивать элементы списка и даже могу добиться некоторых базовых анимаций при захвате элемента списка:

const App = () => {
  const [myArray, setMyArray] = React.useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
  const [draggedIndex, setDraggedIndex] = React.useState(-1);

  const onDragStart = (e, index) => {
    setDraggedIndex(index);
    const emptyDiv = document.createElement('div');
    emptyDiv.style.width = '0px';
    emptyDiv.style.height = '0px';
    e.dataTransfer.setDragImage(emptyDiv, 0, 0);
    e.currentTarget.className = 'draggable';
  };

  const onMouseDown = (e) => {
    e.currentTarget.className = 'draggable';
  };
  const onMouseUp = (e) => {
    e.currentTarget.className = 'listItem';
  };

  const onDragOver = (e, index) => {
    e.preventDefault();
    if (draggedIndex === -1 || draggedIndex === index) {
      return;
    }
    let items = myArray.filter((item, i) => i !== draggedIndex);
    items.splice(index, 0, myArray[draggedIndex]);
    setMyArray(items);
    setDraggedIndex(index);
  };

  const onDragEnd = (e) => {
    setDraggedIndex(-1);
    e.target.className = 'listItem';
  };

  return (
    <div className = "App">
      {myArray.map((x, i) => (
        <div
          className = "listItem"
          draggable
          key = {x}
          onDragStart = {(e) => onDragStart(e, i)}
          onDragOver = {(e) => onDragOver(e, i)}
          onDragEnd = {onDragEnd}
          onMouseDown = {onMouseDown}
          onMouseUp = {onMouseUp}
        >
          <h3>hello - {x}</h3>
        </div>
      ))}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
.App {
  text-align: center;
  align-items: center;
  display: flex;
  flex-direction: column;
}
.listItem {
  border: 2px solid black;
  margin: 5px;
  width: 400px;
  cursor: grab;
  transform: scale(100%);
  transition: transform 0.3s ease-in-out;
}

.draggable {
  border: 2px solid green;
  margin: 5px;
  width: 400px;
  cursor: grab;
  transform: scale(108%);
  transition: transform 0.3s ease-in-out;
}

.listItem:-moz-drag-over {
  cursor: pointer;
}
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id = "root"></div>

Могу ли я получить помощь с анимацией CSS, чтобы сделать движения элементов списка более плавными во время перетаскивания, чтобы они выглядели менее прерывистыми? Цель состоит в том, чтобы добиться следующего эффекта: когда я перетаскиваю элемент, он плавно перемещается вверх/вниз, а перетаскиваемый элемент плавно перемещается в противоположном направлении.

РЕДАКТИРОВАТЬ 1:

Please check the code snippet, run it, and try to drag the list items to understand my requirements.

По сути, я хочу анимировать переход перетаскиваемого элемента в зависимости от того, в каком направлении (вверх или вниз) перетаскивается элемент. Теоретически, при перетаскивании элемента вверх он может применить класс, например .dragged-up, и этот класс будет иметь анимацию/переход, который создаст иллюзию перемещения этого элемента из более низкого положения в более высокое.

Тот же принцип можно применить к элементам выше и ниже перетаскиваемого элемента. Например, если перетаскиваемый элемент перемещается сверху вниз, может быть применен другой класс, например .listItem-down, и этот класс может содержать противоположную анимацию. Кроме того, я подозреваю, что у него должен быть более низкий z-индекс, чтобы он отображался под перетаскиваемым элементом.

Не уверен, что это самый эффективный подход и возможно ли это сделать вообще. До сих пор, пытаясь реализовать что-то подобное, у меня возникали проблемы с перекрытием элементов, и в результате функция события выполнялась не в том div, вызывая некоторые нежелательные эффекты.

Некоторая помощь и рабочий фрагмент будут высоко оценены!

Просто отметим: я не изучал, почему, и не вижу никаких сообщений об ошибках, но что-то в вашем фрагменте не работает в текущем Safari (16.1); событие щелчка срабатывает, чтобы дать небольшую анимацию «импульс», но операции перетаскивания - нет. У меня отлично работает как в хроме, так и в Firefox.

Daniel Beck 20.01.2023 22:33

Ах, это дополнительная проблема, которую я упустил из виду, я предположил, что из-за простоты примера это не должно быть проблемой. Я тоже только что заметил, что работает только при перетаскивании с помощью мыши, но не тачскрина на телефоне. Интересно, будет ли логика кода такой же для мобильных устройств, просто другие события, такие как onTouchMove вместо перетаскивания?

Sebastian Meckovski 20.01.2023 22:40

Ах, теперь я снова думаю об этом и понял, что, возможно, Safari запускает разные события при нажатии? Например, OnTouch вместо onClick

Sebastian Meckovski 20.01.2023 22:48

Нужно ли в решении использовать setMyArray для перестановки, а также нужно ли вам иметь возможность устанавливать элементы, просто расширяя массив, или их можно устанавливать статически?

LazyJ 20.01.2023 23:30

Что касается setMyArray - изначально я думал, что да, но, поскольку вы спросили, я не уверен, что это лучший подход. Это может помочь, если я объясню общую картину. Я создаю компонент, который будет иметь раскрывающийся список и список. Список будет заполнен значениями, выбранными из раскрывающегося списка. Список элементов в раскрывающемся списке будет статическим. Массив списка будет расширяемым, а также переупорядочиваемым путем перетаскивания. Компонент будет частью формы. При создании новой формы мы бы начали с пустого списка. Мы также должны иметь возможность загружать форму из формы API и восстанавливать список из данных.

Sebastian Meckovski 20.01.2023 23:55

Не уверен, что это имеет смысл, но в конечном итоге компонент должен иметь возможность получать массив элементов, пользователь должен иметь возможность переупорядочивать элементы и передавать переупорядоченные данные обратно. Дайте мне знать, если вам нужна дополнительная информация

Sebastian Meckovski 20.01.2023 23:59
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
3
6
196
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Этот ответ вдохновлен этим решением и пытается сделать значительно упрощенный перенос своих основных идей на функциональные компоненты React, которые работают для перетаскиваемых элементов в случае использования.

В опубликованном примере порядок элементов в массиве обновляется при каждом перетаскивании. Чтобы создать переход при изменении порядка, можно обнаружить разницу до и после изменения для каждого элемента и использовать ее в качестве начальной и конечной точек анимации.

В следующем подходе каждому элементу назначается ключ ref, чтобы отслеживать обновления и проверять изменения в их отображаемой позиции с помощью getBoundingClientRect в useLayoutEffect, чтобы можно было предпринять дальнейшие действия до того, как браузер перерисует экран.

Чтобы вычислить различия, положение элементов в последнем рендере prevPos сохраняется отдельно как другое ref, чтобы оно сохранялось между рендерами. В этом упрощенном примере проверяется и вычисляется только top позиция для разницы, чтобы создать смещение для translateY.

Затем, чтобы организовать переход, requestAnimationFrame вызывается два раза, при этом первый кадр визуализирует элементы в смещенных позициях (начальная точка, со смещением в translateY), а второй — в новых естественных позициях (конечная точка, с 0 в translateY). ).

Хотя на данный момент useLayoutEffect уже обрабатывает анимацию, как и ожидалось, тот факт, что onDragOver очень часто запускает и обновляет состояние, может легко вызвать ошибки в отображении движения.

Я попытался реализовать некоторую базовую отмену дребезга для обновления массива состояний и ввел еще один useEffect для обработки обновлений с отменой дребезга, но кажется, что эффекты могут иногда быть нестабильными.

Хотя многое еще можно было бы улучшить, вот экспериментальный пример:

const App = () => {
  const [myArray, setMyArray] = React.useState([
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  ]);
  const [draggedKey, setDraggedKey] = React.useState(null);
  const [pendingNewKey, setPendingNewKey] = React.useState(null);
  const elRef = React.useRef({});
  const keyInAnimation = React.useRef(null);
  const prevPos = React.useRef({});

  // 👇 Attempt to debounce update of array
  React.useEffect(() => {
    if (
      pendingNewKey === null ||
      draggedKey === null ||
      draggedKey === pendingNewKey ||
      keyInAnimation.current === draggedKey
    )
      return;
    const updateArray = () => {
      setMyArray((prev) => {
        const prevIndex = prev.findIndex((x) => x === draggedKey);
        const newIndex = prev.findIndex((x) => x === pendingNewKey);
        const newArray = [...prev];
        newArray[prevIndex] = pendingNewKey;
        newArray[newIndex] = draggedKey;
        return newArray;
      });
    };
    const debouncedUpdate = setTimeout(updateArray, 100);
    return () => clearTimeout(debouncedUpdate);
  }, [pendingNewKey, draggedKey]);

  React.useLayoutEffect(() => {
    Object.entries(elRef.current).forEach(([key, el]) => {
      if (!el) return;

      // 👇 Get difference in position to calculate an offset for transition
      const { top } = el.getBoundingClientRect();
      if (!prevPos.current[key] && prevPos.current[key] !== 0)
        prevPos.current[key] = top;
      const diffTop = Math.floor(prevPos.current[key] - top);
      if (diffTop === 0 || Math.abs(diffTop) < 30) return;
      prevPos.current[key] = top;
      el.style.transform = `translateY(${diffTop}px)`;
      el.style.transition = 'scale 0.3s ease-in-out, transform 0s';

      // 👇 First frame renders offset positions, second the transition ends
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          if (!el) return;
          el.style.transform = `translateY(0px)`;
          el.style.transition =
            'scale 0.3s ease-in-out, transform 100ms ease-out';
        });
      });
    });
  }, [myArray.toString()]);

  const onDragStart = (e, key) => {
    keyInAnimation.current = key;
    setDraggedKey(key);
    const emptyDiv = document.createElement('div');
    emptyDiv.style.width = '0px';
    emptyDiv.style.height = '0px';
    e.dataTransfer.setDragImage(emptyDiv, 0, 0);
    e.currentTarget.className = 'draggable';
  };

  const onMouseDown = (e) => {
    e.currentTarget.className = 'draggable';
  };
  const onMouseUp = (e) => {
    e.currentTarget.className = 'listItem';
  };

  const onDragOver = (e, key) => {
    e.preventDefault();
    if (draggedKey === null) return;
    if (draggedKey === key) {
      keyInAnimation.current = key;
      setPendingNewKey(null);
      return;
    }
    if (keyInAnimation.current === key) {
      return;
    }
    keyInAnimation.current = key;
    setPendingNewKey(key);
    // 👇 Attempt to reduce motion error but could be unnecessary
    Object.values(elRef.current).forEach((el) => {
      if (!el) return;
      el.style.transform = `translateY(0px)`;
      el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
    });
  };

  const onDragEnd = (e) => {
    setDraggedKey(null);
    setPendingNewKey(null);
    keyInAnimation.current = null;
    e.target.className = 'listItem';
  };

  return (
    <div className = "App">
      {myArray.map((x) => (
        <div
          className = "listItem"
          draggable
          key = {x}
          onDragStart = {(e) => onDragStart(e, x)}
          onDragOver = {(e) => onDragOver(e, x)}
          onDragEnd = {onDragEnd}
          onMouseDown = {onMouseDown}
          onMouseUp = {onMouseUp}
          ref = {(el) => (elRef.current[x] = el)}
        >
          <h3>hello - {x}</h3>
        </div>
      ))}
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector("#root"));
.App {
  text-align: center;
  align-items: center;
  display: flex;
  flex-direction: column;
  isolation: isolate;
  gap: 15px;
}
.listItem {
  border: 2px solid black;
  margin: 5px;
  width: 400px;
  cursor: grab;
  z-index: 1;
  transition: scale 0.3s ease-in-out;
  background-color: white;
}

.draggable {
  border: 2px solid hotpink;
  margin: 5px;
  width: 400px;
  cursor: grab;
  scale: 108%;
  z-index: 10;
  transition: scale 0.3s ease-in-out;
  background-color: white;
}

.listItem:-moz-drag-over {
  cursor: pointer;
}
<div id = "root"></div>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

Раствор твердый. Я не думаю, что вы могли бы помочь мне с красным крестом мыши, когда я перетаскиваю? Не могу найти, почему это так

Sebastian Meckovski 06.02.2023 00:17

@SebastianMeckovski Вероятно, это связано с тем, что контейнер не является целью onDragOver, и это следует предотвратить, добавив в контейнер событие, такое как onDragOver = {(e) => e.preventDefault()} (здесь контейнер «Приложение»). Однако в этом примере, поскольку перетаскивание вступает в силу только при перетаскивании элемента, я думаю, было бы неплохо рассмотреть возможность сохранения значка креста, поскольку он указывает эффективную область для перетаскивания.

John Li 06.02.2023 04:52

Вы были правы, добавив onDragOver = {(e) => e.preventDefault()}. предотвращает превращение мыши в красный крест, я на самом деле предпочитаю это. Это на шаг ближе. По-прежнему превращается в значок красного креста при наведении курсора на границу элемента списка.

Sebastian Meckovski 06.02.2023 10:50

@SebastianMeckovski Спасибо за отзыв, мне кажется, в Firefox все в порядке, но когда я пытаюсь использовать Chrome, он действительно показывает значок креста и снова мигает. Вероятно, из-за различий в способах обработки браузерами (относительно новой) функции перетаскивания. До сих пор я думаю, что если мы добавим h3 { pointer-events: none } в CSS, это предотвратит это для границы h3 (текстового поля «Привет»), но поскольку мы не можем добавить то же самое ко всему элементу, не уверен, что это будет можно указать некоторым браузерам отключить это поведение.

John Li 07.02.2023 00:10

Другие вопросы по теме