Вытягивающаяся карточка

Ilya Lobanov
Яндекс.Карты Mobile
3 min readJan 25, 2019

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

Apple Maps, Stocks and Voice Memos

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

В качестве контента для более общего случая мы будем использовать абстрактный DrawerViewContent, который подойдет не только для UIScrollView, но и для любого UIView. Для пейджинации нам потребуется от контента информация о смещении: contentOffset и contentInset. Саму карточку мы назовем DrawerView. Для обработки жестов с контента будем использовать DrawerViewContentListener. За отправку событий DrawerViewContentListener будет отвечать тот, кто реализует DrawerViewContent.

Кроме того, нам необходимо обрабатывать не только жесты пришедших с контента, но и с хедера. Обработка жестов с хедера будет похожа на обработку для скролла из предыдущей статьи: в начале жеста мы будем отменять текущую анимацию, а в конце — запускать анимацию, учитывая скорость жеста. Нужно не забыть поделить скорость жеста на 1000, так как у UIPanGestureRecognizer она представлена в pt/s.

Чтобы хедер нельзя было утащить за пределы опорных точек, будем ограничивать смещение origin с помощью функции trimTargetHeaderOrigin:

Для обработки жестов с контента будем использовать DrawerViewContentListener. За отправку событий должен отвечать сам контент. Если в качестве контента используется UIScrollView, то нам будет достаточно проксировать методы UIScrollViewDelegate в соответствующие методы DrawerViewContentListener. В случае использования статического UIView можно вообще не использовать DrawerViewContentListener и распространить действие UIPanGestureRecognizer с хедера на всю карточку.

Как и в случае с хедером введем состояние для контента ContentState. Оно нам понадобится, чтобы правильно обновлять origin. В методе drawerViewContentDidScroll будем менять состояние на .dragging и отменять текущую анимацию, если необходимо:

В методе drawerViewContentDidScroll мы должны синхронизировать contentOffset с origin карточки. С одной стороны, у нас должна быть возможность вытащить карточку за контент. Для этого мы проверяем условие:

diff < 0 && origin > limits.lowerBound

А с другой стороны, возможность свернуть карточку, когда contentOffset становится меньше минимального значения (-contentInset.top). Для этого мы проверяем условие:

diff > 0 && drawerViewContent.contentOffset.y < -drawerViewContent.contentInset.top

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

Кроме этого, нужно на время отписаться от контента, чтобы изменение contentOffset не привело к рекурсивному вызову drawerViewContentDidScroll.

Обработка завершения жеста тоже немного усложнится: мы будем изменять origin только в том случае, если карточка не находится в развернутом состоянии: то есть когда origin > limits.lowerBound.

Поиск опорной точки будет немного изменен. Мы будем так же искать ближайшую к проекции опорную точку. Но в случае, когда силы для перехода на следующую точку будет недостаточно, мы все равно выполним переход. Это нужно для того, чтобы карточку можно было свернуть или развернуть, не прилагая большого усилия. Особенно это важно для iPad с большим экраном и большими карточками. Для этого мы проверяем условие (projectionAnchor — origin) * velocity < 0, которое показывает, что направление жеста и направление перехода отличаются, и в этом случае мы сделаем переход искусственно.

Ну и теперь осталось реализовать анимацию. Для этого с помощью POPPropertyAnimation:customPropertyRead:write: будем менять анимированно параметр origin, изменение которого в свою очередь повлекут изменение позиции всего containerView.

Данного функционала с пейджинацией уже достаточно для вытягивающихся карточек. Кроме этого, на его основе можно создавать более удобные высокоуровневые классы для более специфичного использования: например, с удобным API для выставления опорных точек или с автоматическим учетом safeAreaInsets для iPhone X.

Пример с кодом можно найти на GitHub: https://github.com/super-ultra/UltraDrawerView

--

--