Свайп плашек

Свайп плашек как в почтовых клиентах, когда появляются кнопки "в архив" и "удалить" под самой плашкой, с помощью javascript.

Пример того что такое плашки

Общий принцип заключается в том, что имеется два html-дива, расположенные друг над другом. В нижнем по углам стоят кнопки, а в верхнем плашка, которую мы будем сдвигать, обнажая нижележащие кнопки. Сдвигать будем с помощью css-стиля translateX.

Свайп базируется на трех javascript-событиях touchstart, touchmove, touchend. Это момент прикосновения, передвижение и отпускание. В момент прикосновения однократно возникает событие touchstart. По мере перетаскивания браузер постоянно генерит событие touchmove и сообщает текущие координаты пальца. В момент отпускания однократно возникает событие touchend и в нем уже нет информации о текущем положении пальца, поэтому последнее положение пальца нужно сохранять у себя в переменных заранее.

Кроме того список плашек может быть прокручиваемым по вертикали, и тут появляется событие scroll. Итого у нас есть четыре программных точки, которые нужно продумать.

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

Кроме того есть два нюанса, которые нужно учесть.

Первый заключается в том, что микродвижения пальца сразу видимы по движению плашки и выглядит и ощущается это не красиво. Для этого нужно выполнять две компенсации. В браузере chrom было бы достаточно одной, поскольку они видимо уже продумали и побороли вертикальные микродвижения. Но вот браузер firefox честно дергает скроллируемый блок, пока палец ложится на экран и увеличивает площадь соприкосновения и это будет мешать дальнейшему свайпу. Вернемся к двум компенсациям.

1. На событии touchmove не сдвигать блок по горизонтали пока палец не сдвинется на какое-то минимальное число пикселей. Экспериментальным путем выбрано 16 пикселей.

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

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

Второй нюанс, который нужно учесть, заключается в том, что нельзя одновременно и скролить и двигать плашку по горизонтали. Поэтому если мы определили что начался горизонтальный свайп, то нужно заблокировать вертикальный скролл. А если начался вертикальный скролл, то нужно заблокировать горизонтальный свайп. Для этого нужна пара переменных, которые мы будем устанавливать в true.

В результате получаем следующее описание кода.

Событие scroll:
1. Проверяем флаг, обозначающий что начался свайп и если он установлен, откатываем скоролл и завершаем код.
2. Вычисляем сдвиг пальца по вертикали и если он был менее 5 пикселей, то откатываем скролл в старую позицию и завершаем код.
3. Если же сдвиг оказался больше 5 пикселей, то скролл выставляем в старый скролл плюс сдвиг пальца и устанавливаем переменную блокирующую горизонтальный свайп в true.

Событие touchstart:
1. Запоминаем текущее состояние вертикальной прокрутки.
2. Запоминаем координаты прикосновения X и Y - для дальнейших вычислений.
3. Сбрасываем флаг скролла. Потому что скроллирование его устанавливает, но не сбрасывает.

Событие touchmove:
1. Вычисляем и запоминаем смещение пальца по вертикали. Это нужно для вертикального скролла.
2. Проверяем переменную что начался вертикальный скролл и если да, то завершаем код.
3. Вычисляем сдвиг пальца по оси X, и если меньше 16 и не начался свайп завершаем код. Если свайп начался, то продолжаем код, иначе будет заклинивание если приблизились к начальной точке. Сдвиг запоминаем в переменной в плашке - она пригодится при событии отпускания.
4. Устанавливаем флаг свайпа.
5. Пробегаемся по всем плашкам и сбрасываем сдвинутые.
6. Берем старую позицию плашки по оси X и добавляем к ней смещение пальца. Старая позиция будет либо в нуле, либо нет если плашка сдвинута для показа кнопки под ней.

Событие touchend:
1. Проверяем переменную что начался вертикальный скролл и если да, то завершаем код.
2. Узнаем величину левой и правой стоп-области, там где плашка фиксируется в сдвинутом состоянии.
3. Если плашка заехала в позицию меньше минимальной, то ставим минимальную.
4. Если плашка заехала в позицию больше максимальной, то ставим максимальную.
5. Иначе сбрасываем ее в 0.
6. Запоминаем позицию в переменной. Это нужно для свайпа на место. И сдвигаем плашку с помощью css.
7. Сбрасываем переменную обозначающую начавшийся горизонтальный свайп в false.
8. Сбрасываем переменную обозначающую начавшийся скролл в false.

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

Имя текущей плашки которую свайпают: plashka
Имя объекта с вертикальной прокруткой: scroller
Прокрутка на момент прикосновения: old_scroll_top
Флаг начала свайпа: swipe_flag
Флаг начала скроллинга: scroll_flag
Координата прикосновения X: touch_start_x
Координата прикосновения Y: touch_start_y
Сдвиг плашки: swipe_pos
Сдвиг пальца по горизонтали: swipe_dx (в плашке храним)
Сдвиг пальца по вертикали: swipe_dy (храним в скроллере)

document.querySelectorAll( "body.logged .pages-grid" ).forEach( function( scroller ) {
    scroller.ticking = false;
    scroller.addEventListener( "scroll", function( e ) {
        if ( ! scroller.ticking ) {
            scroller.ticking = true;
            window.requestAnimationFrame( function() {
                scroller.ticking = false;
            } );
        }
        if ( scroller.swipe_flag ) {
            scroller.scrollTop = scroller.old_scroll_top;
            return;
        }
        if ( Math.abs( scroller.swipe_dy ) < 6 ) {
            scroller.scrollTop = scroller.old_scroll_top;
            return;
        } else {
            scroller.scrollTop = scroller.old_scroll_top - scroller.swipe_dy;
        }
        scroller.scroll_flag = true;
    } );
} );


let plashka = product.querySelector( ".bp" );

plashka.addEventListener( "touchstart", function( e ) {
    let scroller = product.closest( ".pages-grid" );
    this.scroller = scroller;
    scroller.old_scroll_top = scroller.scrollTop;
    scroller.touch_start_x = e.touches[0].clientX;
    scroller.touch_start_y = e.touches[0].clientY;
    this.scroller.scroll_flag = false;
} );

plashka.addEventListener( "touchmove", function( e ) {
    this.scroller.swipe_dy = e.touches[0].clientY - this.scroller.touch_start_y;
    if ( this.scroller.scroll_flag ) {
        return;
    }
    this.swipe_dx = e.touches[0].clientX - this.scroller.touch_start_x;
    if ( Math.abs( this.swipe_dx ) < 16 && ! this.scroller.swipe_flag ) {
        return;
    }
    this.scroller.swipe_flag = true;
    this.scroller.querySelectorAll( ".bp" ).forEach( function( other ) {
        if ( plashka != other ) {
            other.style.transform  = "";
            other.swipe_pos        = 0;
        }
    } );
    let translate_x = +this.swipe_pos + this.swipe_dx;
    this.style.transform = `translateX(${translate_x}px)`;
    this.style.transition = "0s"; // откл анимацию сдвига
} );
    
plashka.addEventListener( "touchend", function( e ) {
    if ( this.scroller.scroll_flag ) {
        return;
    }
    let left  = -this.scroller.getAttribute( "data-min-swipe-left" );
    let right = +this.scroller.getAttribute( "data-min-swipe-right" );
    if ( this.swipe_pos + this.swipe_dx < left ) {
        this.swipe_pos = left;
    } else if ( this.swipe_pos + this.swipe_dx > right ) {
        this.swipe_pos = right;
    } else {
        this.swipe_pos = 0;
    }
    this.style.transform = `translateX(${this.swipe_pos}px)`;
    this.style.transition = ""; // вкл анимацию сдвига
    this.scroller.scroll_flag = false;
    this.scroller.swipe_flag = false;
} );

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

30 апреля 2023, 14:34
Oleg)
Классная статья!
Ответить
Комментировать
Закрыть
Сумма:
0 ₽
После согласования условий заказа мы Вам отправим счёт или ссылку c удобным способом оплаты.
Оформить заказ