Давайте прямо. Вы слышали о материальном дизайне и хотели бы применить эффект ряби в своих собственных приложениях React. Конечно, есть библиотеки, такие как material-ui, которые уже реализовали их, но вам действительно не хочется использовать одну из них. Вам просто нужен один <Ripple>
компонент, который вы можете применить к другим элементам (кнопке, переключателю, элементу списка или чему-то еще).
Вот что я создал - реакция-прикосновение-рябь. Он легкий, простой в использовании, и вы можете использовать его где угодно для вашего удобства. Если вам все равно, как это работает, вы можете прекратить читать сейчас и взять его в npm.
Для людей, которые готовы погрузиться в это глубже, я покажу вам, как я создаю это шаг за шагом. Очевидно, что здесь не будут рассмотрены все детали, но вы всегда можете проверить исходный код, если хотите.
Что будем делать?
Мы разделим на два компонента: Ripple
и RippleWrapper
.
Компонент Ripple
- это сам маленький круг. Он появится, когда вы где-то щелкнете (очевидно, мы хотим, чтобы он отображался именно в том месте, где вы щелкаете), и через короткий промежуток времени рассеется.
Компонент RippleWrapper
- это, по сути, оболочка. Это контейнер для Ripple
(ов), так как у вас может быть несколько ripple
компонентов, если вы быстро нажимаете. Кроме того, мы привяжем всех слушателей к компоненту RippleWrapper
, чтобы он взял на себя ответственность за обработку всей логики.
‹Ripple›
Компонент Ripple
довольно прост. Он состоит из двух этапов: вход и выход.
- На этапе ввода размер ряби становится больше, поэтому мы применим к ней
transform: scale(0)
иtransform: scale(1)
. В то же время мы также увеличиваем его прозрачность, добавляяopacity: 0
кopacity: 0.3
. - На этапе выхода единственное, что нас волнует, - это прозрачность, поэтому устанавливаем
opacity: 0
, и все готово.
Следовательно, мы можем написать некоторый CSS примерно так:
.ripple-entering { opacity: 0.3; transform: scale(1); animation: ripple-enter 500ms cubic-bezier(0.4, 0, 0.2, 1) }
.ripple-exiting { opacity: 0; animation: ripple-exit 500ms cubic-bezier(0.4, 0, 0.2, 1); } @keyframes ripple-enter { 0% { transform: scale(0); } 100% { transform: scale(1); } } @keyframes ripple-exit { 0% { opacity: 1; } 100% { opacity: 0; } }
Мы можем контролировать это в компоненте React react-transition-group
. Что он делает, так это когда компонент Ripple
монтируется, он запускает событие onEnter
, которое мы можем взять под контроль и выполнить анимацию оттуда. Кстати, react-transition-group
- это в основном единственная зависимость, которую мы используем помимо react
.
class Ripple extends React.Component {
state = {
rippleEntering: false,
rippleExiting: false,
};
handleEnter = () => {
this.setState({ rippleEntering: true, });
}
handleExit = () => {
this.setState({ rippleExiting: true, });
}
render () {
const { className, rippleX, rippleY, rippleSize, color, timeout, ...other } = this.props;
const { rippleExiting, rippleEntering } = this.state;
return (
<Transition
onEnter={this.handleEnter}
onExit={this.handleExit}
timeout={timeout}
{...other}
>
<span className={wrapperExiting ? 'ripple-exiting' : ''}>
<span
className={rippleEntering ? 'ripple-entering' : ''}
style={{
width: rippleSize,
height: rippleSize,
top: rippleY - (rippleSize / 2),
left: rippleX - (rippleSize / 2),
backgroundColor: color,
}}
/>
</span>
</Transition>
);
}
}
Ripple
принимает три важных реквизита: rippleX
, rippleY
(это позиция, в которой он должен появиться) и rippleSize
, который указывает чистый объем компонента. Эти свойства вычисляются в компоненте RippleWrapper
, поэтому далее мы поговорим о том, как их вычислить.
Вы определенно можете написать более надежный код, чем этот, но, как я упоминал ранее, я даю вам только суть или основную структуру моей реализации.
‹RippleWrapper›
Обработка событий
Сначала мы связываем интересующие нас события.
class RippleWrapper extends React.Component { state = {
rippleArray: [], nextKey: 0}; handleMouseDown = (e) => { this.start(e); } handleMouseUp = (e) => { this.stop(e); } handleMouseLeave = (e) => { this.stop(e); } handleTouchStart = (e) => { this.start(e); } handleTouchEnd = (e) => { this.stop(e); } handleTouchMove = (e) => { this.stop(e); } render () { <TransitionGroup component="span" enter exit onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseLeave={this.handleMouseLeave} onTouchStart={this.handleTouchStart} onTouchEnd={this.handleTouchEnd} onTouchMove={this.handleTouchMove} > {this.state.rippleArray} </TransitionGroup> } }
Когда срабатывают два события mousedown
или touchstart
, нам нужно создать новую рябь. Итак, мы назовем this.start
. Когда срабатывают остальные четыре события, это означает, что пульсация должна быть удалена, поэтому мы просто вызовем this.stop
.
Некоторые из вас могут спросить: эти события (по крайней мере, некоторые из них) на самом деле запускаются компонентом Ripple
, а не непосредственно RippleWrapper
, собираемся ли мы их фиксировать? Да, из-за распространения событий. Событие сработает для предков целевого элемента в той же иерархии вложенности, поэтому обработчики событий, которые мы установили на RippleWrapper
, в конечном итоге сработают, и все будет хорошо, не так ли. Вообще-то, нет. Использование всплывающих событий приводит к огромной, но очень сложной ошибке, которую мы обсудим позже.
start()
и stop()
В функции start
мы должны выполнить три задачи:
- Вычислите координаты того места, где именно происходит событие, и затем передайте их
Ripple
какrippleX
иrippleY
. - Определите, насколько большой должна быть пульсация, и передайте ее
Ripple
какrippleSize
. - Создайте рябь, используя только что рассчитанные нами факторы.
functionstart (e) { const element = ReactDOM.findDOMNode(this); const rect = element ? element.getBoundingClientRect() : { left: 0, right: 0, width: 0, height: 0, }; let rippleX, rippleY, rippleSize;
// 1. calculate rippleX and rippleY const clientX = e.clientX ? e.clientX : e.touches[0].clientX; const clientY = e.clientY ? e.clientY : e.touches[0].clientY; rippleX = Math.round(clientX - rect.left); rippleY = Math.round(clientY - rect.top);
// 2. calculate ripple size const sizeX = Math.max(Math.abs((element ? element.clientWidth : 0) - rippleX), rippleX) * 2 + 2; const sizeY = Math.max(Math.abs((element ? element.clientHeight : 0) - rippleY), rippleY) * 2 + 2; rippleSize = Math.sqrt(Math.pow(sizeX, 2) + Math.pow(sizeY, 2));
// 3. create ripple this.createRipple({ rippleX, rippleY, rippleSize }); }
Обратите внимание, что для rippleX
и rippleY
нам нужны относительные координаты, поэтому значения e.clientX
и e.clientY
нельзя использовать напрямую, поскольку они являются« абсолютными координатами». Вместо этого мы вычитаем их на абсолютные координаты самого RippleWrapper
. (мы можем получить эти координаты с помощью getBoundingClientRect
)
По поводу stop
сказать особо нечего. Единственное, что мы делаем, это удаляем первый элемент из rippleArray
.
function stop (e) {
const { rippleArray } = this.state;
if (rippleArray && rippleArray.length) {
this.setState({
rippleArray: rippleArray.slice(1),
});
}
}
createRipple ()
Теперь мы можем, наконец, собрать их все вместе, создать компонент Ripple
и добавить его в наш rippleArray
.
function createRipple (params) {
const { rippleX, rippleY, rippleSize } = params;
let { rippleArray } = this.state
rippleArray = [
...rippleArray,
<Ripple
key={this.state.nextKey}
rippleX={rippleX}
rippleY={rippleY}
rippleSize={rippleSize}
/>
];
this.setState({
rippleArray: rippleArray,
nextKey: this.state.nextKey + 1,
});
}
Примечание для nextKey
здесь. React просит нас иметь уникальный ключ для каждого элемента списка, чтобы повысить эффективность согласования. Таким образом, мы будем увеличивать nextKey
на 1 каждый раз, когда создадим рябь.
Букашка
Как я упоминал ранее, существует критическая ошибка, если вы полагаетесь на всплытие событий. То есть, когда вы щелкаете элемент с большей скоростью, событие «onclick» никогда не запускается.
Сначала я подумал, что это как-то связано с всплыванием событий, но позже, когда я провел некоторую отладку и обнаружил, что событие никогда не запускалось, я понял, что это поведение браузера, которое имеет какое-то отношение к механизму событий.
Без сомнения, я обратился к рабочему проекту w3, пытаясь понять, при каких условиях сработает событие onclick. И вот что нам дала w3:
Событию
click
МОГУТ предшествовать событияmousedown
иmouseup
в том же элементе, без учета изменений между другими типами узлов (например, текстовыми узлами).
То есть, mousedown
и mouseup
должны запускаться для одного и того же элемента, прежде чем onclick
запустится. И вот через что мы прошли, когда мы щелкаем элемент быстро , мы фактически запускаем mousedown
на предыдущем Ripple
, который вскоре после этого уничтожается , а затем mouseup
срабатывает на текущем Ripple
. Они срабатывали по разным элементам, и в результате onclick
просто не запускался.
Что касается решения, эту проблему можно решить, добавив одну строку кода CSS в элемент Ripple
:
pointer-events: none;
Почему это работает? Когда pointer-events
установлен на none
:
Элемент никогда не является целью событий указателя; однако события указателя могут быть нацелены на его дочерние элементы, если для этих потомков
pointer-events
установлено какое-то другое значение. В этих обстоятельствах события указателя будут запускать прослушиватели событий на этом родительском элементе по мере необходимости на их пути к / от потомка во время фаз захвата / пузыря события.
Следовательно, все события запускаются в компоненте RippleWrapper
, и это нас полностью устраивает, поскольку мы все равно обрабатываем эти события на RippleWrapper
.
Конец
Вот в основном то, как создать эффект ряби в материальном дизайне с помощью React. Но если вы реализуете это в реальном мире, есть еще кое-что, о чем вам следует позаботиться.
touch
события иногда могут происходить невероятно быстро, что означает, что некоторыеRipple
будутstop()
еще до того, как они будут созданы и отображены. Поэтому необходимо добавить тайм-аут или дебаунс.- Событие
touchstart
начинается сmousedown
. Таким образом, прикосновение к сенсорному устройству приведет к созданию двухRipple
. Чтобы исправить это, нам нужен флаг, чтобы игнорироватьmousedown
.
Вот и все! Конечно, есть некоторые детали, на которые вы должны обратить внимание, но я не буду останавливаться на них в этом посте. Если вам интересно, вы всегда можете обратиться к исходному коду на github. Оставьте звезду, если она вам нравится. Спасибо.