Что такое power bottom
Power Bottom Definition: Here’s the REAL Meaning
Power Bottom Definition
Power bottom is a term that gets thrown around a lot by people in our community, yet few truly understand what this phrase really means. Well, I’m here to give you the definition so that you have the lingo right.
In case you are wondering, I am a power bottom and have been one for most of my sexual life. The first time I got pounded was when I was 18 years old, in the back of an abandoned van. Now at 38, I’ve been one ever since.
I’m going to give you the definition of a power bottom first. Then, I’ll touch upon several of the stereotypes connected with this term. Finally, I’ll walk you through several truths and misnomers.
Are you ready? Let’s jump right in!
What is a Power Bottom?
If you are a true power bottom, you are gay man who has a huge appetite for being penetrated by another guy (or guys). You can last for extended periods of time without having to take breaks.
Power bottoms tend to last longer in the sack when the top is confident during intense jack-hammering (read between the lines). Power bottoms are ready to take it day or night and commonly like to skip a lot of foreplay and get right down to business.
Power Bottom Myths
Let’s first dispense with some of the common myths that are often associated with being a power bottom. While I can’t list them all, here are some of the biggies.
A lot of people see to think that guys like me who power bottom are “queens” or “big girls”. That’s totally false. I would argue that guys who mostly or exclusively bottom are perhaps some of the most masculine guys on the planet. This myth can be traced to harmful stereotypes about gay men – pure and simple.
I’ve had friends say that power bottoms are bigtime selfish whores. This one is BS too. That’s because being a power bottom isn’t so much about the activity but instead, about the mindset.
Just because we like to bend over doesn’t mean we can’t or won’t top. It does mean that we prefer to riding a guy and doing so for long periods of time.
Perhaps the worst stereotype associated with this bit of lingo relates to physical pain. For some stupid reason, a lot of gay tops have it in their mind that power bottoms don’t feel pain.
I’m here to tell you that’s total B.S. because we do. It DOES hurt if you just jam it in or suddenly yank it out. Nothing pisses me off more than a top thinking he can do whatever he wants to me because the guy thinks I’m impervious to pain, you know?
And hate to break it to you but not all of us are hooked on crystal meth, causing us to get “tina d*ck”. I don’t know if that myth came from videos or an urban legend but it’s crap.
Containing on with the stereotypes, power bottoms can be submissive but that doesn’t mean they are totally submissive. Sure, I suppose if you hypnotize one of us, we could turn into some freakish zombie bottom but in the real world, that’s not been my experience.
Finally, not all guys like me who are power bottoms are slutty. I hate when gay men think this but apparently, many do. Remember, it’s not about the number of guys we can take but instead, our ability to be ready and last for the long term.
To be this type of bottom, it means you have personal POWER. Here, I am talking about a high degree of self-esteem and self-worth. Not the other way around.
Power Bottom Truths
As mentioned earlier, a gay man who identifies as a power bottom has adopted a mindset. This way of thinking takes years to develop and has nothing to do with the top’s size or girth.
Instead, it’s about the pounders ability to jump into action at a moment’s notice, allowing someone like me to be equally ready.
This means we’ve educated ourselves about digestive health, allowing us to do our thing without worry.
On a related note, a true – and I mean absolutely true power bottom knows all about the things that turn off guys who are tops. One of the big ones is never making the mistake of referring to our man holes with effeminate terms.
Another one is understanding the importance of thorough douching!
Bottoming Doesn’t = Power Bottom
What really pisses me off is running into guys on hookup apps like Grindr and Scruff who claim they are power bottoms when they simply aren’t.
Just because you rode a guy for 20 minutes doesn’t entitle you to claim the label. And so what if you can sit on a giant toy! That doesn’t mean you are one either!
So gay men everywhere, please, for the love of all that is good, please stop calling yourself a power bottom when you aren’t.
This is a very special title should be reserved for gay men who have put in years of effort while educating themselves on how to be masters of their craft.
Bottom Sheet, перейдём на «ты»?
Bottom Sheet представлялся мне сложным и недосягаемым. Это был вызов! Я не понимал, с чего начать. Возникало много вопросов: использовать view или view controller? Auto или manual layout? Как анимировать? Как скрывать Bottom Sheet интерактивно?
Но всё изменилось после работы над Bottom Sheet для приложения Joom, где он используется повсеместно. В том числе и в таких критических сценариях, как оплата. Так что могу точно сказать, что в этом компоненте мы уверены. Настолько уверены, что я даже рассказывал о нём на Podlodka iOS crew #7. В рамках воркшопа я показал, как сделать Bottom Sheet, который умеет подстраиваться под размер контента, интерактивно закрывается и поддерживает UINavigationController.
Стоп, но Apple же предоставила системный Bottom Sheet. Зачем писать свой? Действительно, это так, но компонент поддерживается только с iOS 15. А это значит, что полноценно его можно будет использовать только через 2-3 года. К тому же часто требования дизайнеров выходят за рамки стандартных iOS-элементов.
В рамках статьи хочу развеять туман над Bottom Sheet, ответить на вопросы, которыми задавался я сам и предложить один из вариантов реализации. Чтобы в конце вы могли добавить в резюме строчку «Профессионально делаю Bottom Sheet’ы».
Если заинтересовал, то начнём! Создадим простой Bottom Sheet и шаг за шагом его прокачаем.
Научимся подстраиваться под размер контента и закрывать Bottom Sheet.
Добавим интерактивное закрытие, учитывая контент, который скроллится.
Поддержим UINavigationController с навигацией внутри Bottom Sheet.
Часть 1. Адаптируемся под размер контента. Закрываем Bottom Sheet. Базовый дизайн
Стартовый проект
Вот ссылка на стартовый проект на Github. В проекте есть два таргета: BottomSheetDemo и BottomSheet — приложение и библиотека с Bottom Sheet.
RootViewController — это первый экран в приложении. В нём есть всего одна кнопка Show Bottom Sheet. По нажатию покажется ResizeViewController.
Теория. Как показывать Bottom Sheet?
Нам нужна сущность, которая будет управлять показом. Она добавит Bottom Sheet в UI-иерархию, расположит его на экране, учтёт размер контента, будет реагировать на его изменения, позаботится об анимации, и можно будет сделать интерактивное закрытие.
Это похоже на зону ответственности UIPresentationController. С момента появления view controller’а и до момента скрытия, UIKit использует presentation controller для управления процессом показа.
Для его использования надо переопределить modalPresentationStyle и передать presentation controller через transitioningDelegate.
Вооружившись этим знанием, начнём делать Bottom Sheet!
Создадим presentation controller
Для показа Bottom Sheet переопределим modalPresentationStyle и transitioningDelegate. Не забываем, что transitioningDelegate — это weak ссылка, и нам понадобится strong ссылка, чтобы не потерять объект.
Создадим BottomSheetTransitioningDelegate — реализацию transitioningDelegate.
И presentation controller.
Наконец вернёмся в RootViewController и закроем TODO.
Давайте запустим приложение.
Как будто стало только хуже. View controller открывается во весь экран и скрывается за status bar. Мы переопределили системный presentation controller, который показывал view controller красиво, позиционировал его с учетом safeArea. В нашем presentation controller ничего подобного нет, мы никак не указываем положение view controller, давайте исправимся.
Учитываем размер контента
Вернёмся к ResizeViewController. Поле currentHeight отвечает за текущую высоту. Чтобы не создавать лишних протоколов, используем preferredContentSize. Он будет показывать текущий желаемый размер для Bottom Sheet.
В presentation controller переопределим frameOfPresentedViewInContainerView, который отвечает за положение presentedView. В нашем случае presentedView — это view ResizeViewController. containerView — это view, которая содержит presentedView и куда можно добавить, например, тень.
Дополнительно укажем shouldPresentInFullscreen в false, потому что Bottom Sheet покрывает не весь экран.
Посмотрим, что получилось.
Изначальный размер учитывается, но нет реакции на его изменения.
Реагируем на изменение контента
Рассмотрим UIPresentationController. Он реализует UIContentContainer, в котором нам интересен preferredContentSizeDidChange(forChildContentContainer:), который вызывается при изменениях preferredContentSize в дочерних view controller’ах.
Проверяем текущий frame и тот, который мы считаем правильным. Если они разные, то обновляем presentedView.frame. Запустим приложение.
Размер изменяется неравномерно без анимации. Почему? Потому что мы никак не указываем эту анимацию. Добавим анимацию на изменения preferredContentSize в ResizeViewController.
Работает! Но мы никак не можем уйти с Bottom Sheet.
Закрываем Bottom Sheet
Для закрытия добавим тень и по нажатию будем скрывать Bottom Sheet. Создадим обработчик закрытия, через который presentation controller будет сообщать, что готов, чтобы его закрыли.
Передаём его в инициализатор presentation controller.
Для удобства создадим фабрику presentation controller’а.
Которая будет использоваться внутри BottomSheetTransitioningDelegate.
В RootViewController реализуем фабрику и обработчик закрытия. Скрываем presentedViewController, потому что это и есть Bottom Sheet.
Теперь в presentation controller’е сконфигурируем тень с обработчиком скрытия. Добавляем тень перед началом транзишена и убираем после окончания.
Для начала нам понадобится отслеживать состояние presentation controller. Введём поле state, которое будет отвечать за текущее состояние Bottom Sheet. Для отслеживания состояния переопределим методы жизненного цикла транзишена.
Жизненный цикл presentation controller
Далее возникает вопрос, в какой момент добавлять и удалять тень. Добавлять тень будем перед показом Bottom Sheet, а удалять — сразу после скрытия Bottom Sheet.
Остаётся реализовать addSubviews() и removeSubviews().
Добавляем и удаляем тень — addSubviews() и removeSubviews()
Посмотрим, что получилось.
Отлично, теперь у нас добавляется тень, и по нажатию Bottom Sheet закрывается! Но тень появляется и исчезает без анимации.
Анимированный транзишен
Как быть? Тень относится к транзишену и должна анимироваться вместе с ним. Поэтому нам нужно встроиться в transitioning delegate.
Реализуем протокол UIViewControllerAnimatedTransitioning, в котором будет логика для анимированного поднимания и опускания шторки. Ровно такая же, как и у системы, но дополнительно добавим fade-анимацию для тени.
Поднимаем и опускаем шторку через animated transitioning
Для простоты реализуем протокол внутри presentation controller, потому что у него уже есть доступ к нужным UI-элементам.
И не забудем реализовать соответствующие методы в BottomSheetTransitioningDelegate.
Убедимся, что анимация появилась.
Остаётся добавить закругленные края, и наш Bottom Sheet готов!
Закругляем края
Через cornerRadius у presentedViewController в presentation controller. Нам нужно сделать это перед началом транзишена в presentationTransitionWillBegin().
Закругленные! Теперь Bottom Sheet соответствует дизайну!
Что мы сделали в первой части?
Переопределили системный transitioning delegate.
Создали presentation controller.
Добавили тень для скрытия Bottom Sheet через dismiss handler.
Реализовали анимированный транзишен через transitioning delegate.
Поддержали базовый дизайн.
Часть 2. Интерактивное закрытие Bottom Sheet
Стартовый проект
Как и в первой части начинаем со стартового проекта. К проекту добавился pull bar, который подскажет пользователю, что Bottom Sheet можно скрыть не только по нажатию в пустое пространство, но ещё и по swipe-жесту. Так же в ResizeViewController появился scrollView во весь экран. Он нам пригодится для списочных экранов. Остальное из первой части.
Теория. Особенности интерактивного закрытия
Используем UISwipeGestureRecognizer для распознания swipe-жеста. По нему будем начинать закрытие Bottom Sheet.
Но что, если у presented controller уже есть такой жест? Тогда это может приводить к конфликту жестов, потому что непонятно, какой обрабатывать первым.
Но так ли часто у presented controller может быть такой жест? На самом деле постоянно. В современном приложении 99 % экранов списочные. Это означает, что в каждом есть UIScrollView или его наследники: UITableView или UICollectionView, в которых есть тот самый жест. Как же быть?
Давайте разберём два случая, когда UIScrollView нет и он есть.
Если нет, то всё просто — добавляем swipe-жест.
Если есть, то контент может помещаться:
Полностью. Тогда размер Bottom Sheet меньше экрана, и будем закрывать Bottom Sheet сразу по swipe-жесту.
Частично. Тогда swipe может означать так же и скроллинг. Будем считать, что пользователь хочет закрыть Bottom Sheet по swipe’у вниз и, когда контент закончился сверху (нулевой contentOffset).
В случае наличия UIScrollView подписываем на изменения contentOffset, и по ним понимаем, в какой момент можно начинать интерактивное закрытие.
По механике договорились, давайте её реализуем.
Если UIScrollView нет
То добавляем pan gesture к presentedView. В какой момент это делать? Жест инициирует интерактивное закрытие. А Bottom Sheet может быть закрыт, только если он полностью на экране. Поэтому разумно добавлять жест на окончание показа в presentationTransitionDidEnd(_:).
И напишем функцию, которая добавляет pan gesture заданной view.
Разберём каждое состояние жеста.
began — пользователь только-только начал движение пальца и жест был определен, как pan gesture. Инициируем закрытие Bottom Sheet.
changed — пользователь непрерывно ведёт пальцем по экрану. Скрываем Bottom Sheet пропорционально расстоянию, которое прошёл палец по экрану.
ended — пользователь отпустил палец с экрана. Принимаем решение, закрывать Bottom Sheet или возвращать его в исходное положение.
cancelled — жест был отменён. Возвращаем Bottom Sheet в исходное состояние.
Дополнительно будем использовать UIPercentDrivenInteractiveTransition для передачи состояния транзишена transitioning delegate’у.
Начнём с состояния began. Это подходящий момент для инициализации интерактивного закрытия, потому что это состояние происходит всего один раз. Так же вызываем dismiss у presentingViewController для уведомления UIKit о намерении закрыть Bottom Sheet.
Далее — состояние changed. Изменяем позицию presentedView пропорционально движению пальца по экрану. Рассчитываем расстояние от начальной точки, где жест начался, до текущей. Далее вычисляем прогресс транзишена относительно высоты content view controller, т.е. presentedView.
Когда пользователь отпустил палец, то жест переходит в состояние ended. Нужно определить намерение пользователя: скролл контента или желание закрыть Bottom Sheet. Если пользователь резко опустил палец вниз, пройдя минимальное расстояние, то скорее всего он хотел закрыть Bottom Sheet. Возможна и другая ситуация, если пользователь прошел большое расстояние по экрану и в последний момент с ускорением отпустил палец с экрана вверх. В такой ситуации Bottom Sheet должен вернуться в исходную позицию. Это наводит на мысли о расчёте некого импульса движения, который учитывает ускорение и направление.
Немного физики. Представим, что есть некое тело, которое двигается с постоянной скоростью . Дальше на него подействовало замедление
на расстоянии
. Вопрос — где остановится тело?
Вывод формулы расстояния для тела с замедлением
Напишем формулу для скорости
С отрицательным ускорением скорость станет нулевой, обозначим этот момент времени . Подставим
в формулу скорости:
Далее подставим в формулу расстояния
Окончательно получим формулу для расстояния , где
— начальное положение тела,
— начальная скорость и
— замедление.
Примем Bottom Sheet за тело из задачи выше, когда жест закончился. Через pan gesture узнаем текущую скорость. Пройденное расстояние у нас есть. Замедление подбираем и принимаем за константу . Зная формулу для расстояния
, рассчитаем, где остановится Bottom Sheet под замедлением. Если точка остановки ближе к начальному положению, то отменяем транзишен, иначе завершаем его.
И если жест отменился cancelled, то возвращаемся в исходное положение.
Также не забудем вернуть interactiveTransitioning в transitioning delegate.
Возвращаем interactiveTransitioning в transitioning delegate
Запустим приложение и посмотрим, что получилось.
Вжух-вжух, и мы научили Bottom Sheet закрываться по свайпу вниз! Однако, если контент больше экрана, то scrollView перехватывает жесты.
Разбираем списочные экраны
Несмотря на то, что до этого ResizeViewController и так был списочным, это не помешало нам добавить pan gesture. Всё потому, что у scrollView нет скролла, когда contentSize равен его размеру.
Поэтому рассмотрим случай, когда contentSize больше размера Bottom Sheet, и скролл есть. Подписываемся на contentOffset. Если contentOffset нулевой, и пользователь скроллит вниз, то инициируем закрытие. Когда пользователь отпускает палец с экрана, то решаем отменить или закончить транзишен, как и раньше. Если contentOffset изменяется и пользователь не касается экрана, то ничего не делаем. Это значит, что скролл происходит по инерции.
Для начала нам понадобится признак, который подскажет, что у view controller есть scrollView. Введём для этого протокол.
Для отслеживания изменений contentOffset подписываемся на UIScrollViewDelegate. Но что, если кто-то уже подписался на delegate UIScrollView? Тогда мы затрём предыдущий delegate.
Поэтому будем использовать прокси на UIScrollViewDelegate. Идейно MulticastDelegate реализует UIScrollViewDelegate и проксирует методы delegate заинтересованным сторонам. При этом заботится, чтобы поле delegate не затиралось. В Swift нам приходится определять каждый метод delegate. В Objective-C можно добиться аналогичного результата без реализации всех методов delegate через runtime.
Подписываемся на delegate после окончания транзишена в presentationTransitionDidEnd(_:), как и с pan gesture.
Дальше реализуем UIScrollViewDelegate.
Вспомогательные переменные и хелперы UIScrollView
И хелперы UIScrollView.
Начнём с касания экрана scrollViewWillBeginDragging(:_). В этот момент пользователь только-только начал swipe-жест. Запоминаем это состояние через флаг isDragging.
Далее рассмотрим вспомогательную функцию shouldDragOverlay(following:), в которой определяем, нужно ли обновить прогресс транзишена.
Проверяем, что пользователь сейчас ведёт пальцем по экрану. Если так, то разбираемся, было ли инициировано закрытие.
Если закрытие инициировано и мы где-то посередине транзишена, то продолжаем дальше. Если мы только-только инициировали закрытие, то проверяем, что скролл направлен вниз и контент находится сверху через isContentOriginInBounds. Если мы в начале транзишена, то также проверяем, что скролл направлен вниз и контент сверху.
Дальше разберём, когда contentOffset изменяется и вызывается scrollViewDidScroll(:_).
startInteractiveTransitionIfNeeded() — инициирует интерактивный транзишен, если мы это ещё не сделали.
В scrollViewDidScroll(_:) проверяем, можем ли мы продолжить (начать) интерактивный транзишен. Если можем, то инициируем транзишен через startInteractiveTransitionIfNeeded(). Если не можем, то запоминаем последний contentOffset перед активацией транзишена. Он пригодится дальше.
Помните, как на собеседованиях спрашивали, когда bounds.origin ненулевой? Вероятно, ответ был «когда contentOffset у scrollView ненулевой». Но было непонятно, как это можно использовать на практике, правда? Ниже хорошая возможность оправдать наличие этого знания!
Далее убедимся, что контент прибит к верху, и приравняем contentInset к contentOffset. Измененяем contentOffset через bounds.origin, чтобы не вызывать scrollViewDidScroll(_:). В конце обновляем прогресс транзишена.
Когда пользователь отрывает палец от экрана после скролла, то UIScrollViewDelegate вызывает scrollViewWillEndDragging(_:withVelocity:targetContentOffset:). В нём, как и в обработке состояния pan gesture ended, завершаем или отменяем транзишен.
Через didStartDragging проверяем было ли активно интерактивное закрытие Bottom Sheet перед окончанием скролла.
Если да, то, как и с pan gesture, используем скорость с замедлением, чтобы решить, отменить или закончить переход.
Если нет, то отменяем транзишен. Возможна ситуация, что пользователь начал скрывать Bottom Sheet, а потом вернулся к скроллу контента. В этом случае у транзишена нулевой прогресс, и мы точно хотим его отменить.
Давайте посмотрим, что получилось!
Итак, мы научились работать со всеми размерами Bottom Sheet.
Что мы сделали во второй части?
Поддержали pan gesture и разобрали состояния: began, changed, ended, cancelled.
Реализовали множественную подписку на delegate через MulticastDelegate.
Отслеживаем UIScrollViewDelegate и обновляем состояние транзишена.
Часть 3. Поддержим UINavigationController
Стартовый проект
Начинаем со стартового проекта. В ResizeViewController добавилось две кнопки, которые видны, если есть navigation controller. Первая пушит ResizeViewController с текущей высотой контента. Вторая сворачивает навигационный стек к rootViewController. Остальное из второй части.
Добавим возможность показывать UINavigationController c привычными операции push и pop. Также не забываем про системный интерактивный pop, который хочется поддержать.
Теория. А из коробки заработает?
Можно ли напрямую использовать системный UINavigationController? К сожалению, нет.
Поэтому нам точно понадобится наследник UINavigationController, который сможет отслеживать изменение навигационного стека и обновлять свой собственный preferredContentSize ориентируясь на topViewController.
В presentation controller при отслеживании scrollView нужно учесть, что presentedViewController может быть UINavigationController. И при изменении навигационного стека необходимо извлечь scrollView из текущего topViewController, если он есть.
Последнее. Navigation controller поставляется вместе с версией iOS SDK. Получается, что каждый раз мы работаем с новым компонентом со своими особенностями. И, как мы убедимся дальше, эти особенности себя проявят и нам помешают. Как и что будем с этим делать — обсудим ближе к делу.
Адаптируемся под размер контента
Мы уже делали адаптацию под размер контента в первой части, но с поддержкой navigation controller фича сломалась. Системный navigation controller не учитывает изменения preferredContentSize в полной мере. Поэтому создадим наследника UINavigationController и воспользуемся свойством UIContentContainer.
В updatePreferredContentSize учитываем topViewController и additionalSafeAreaInsets.
Аналогично presentation controller, реагируем на изменение content size через preferredContentSizeDidChange(forChildContentContainer:). Помним, что нужно самим позаботиться об анимации при изменении preferredContentSize. Поэтому добавим анимацию в BottomSheetNavigationController и уберём её из ResizeViewController.
В presentation controller учтём, что presentedViewController может быть UINavigationController. Тогда нужно подписываться на текущий topViewController и на изменения навигационного стека. Обновим setupScrollTrackingIfNeeded().
Для отслеживания изменений навигационного стека подписываемся на delegate через известный нам паттерн MulticastDelegate. Присматриваем за scrollView при показе view controller.
Navigation controller стал реагировать на изменения размера контента, но при переходе назад размер не участвует в анимации.
Анимируем транзишен push и pop
Почему так происходит? Потому что системная реализация navigation controller не учитывает preferredContentSize при изменении навигационного стека. Поэтому нужно обновлять размер контента вместе с изменениями стека навигации.
Для этого введём вспомогательную функцию, через которую будут идти обновления стека вместе с обновлением preferredContentSize. Если возможно, то изменение размера контента делаем анимированно через transitionCoordinator. Важно сначала обновить стек и только потом обновлять размер контента. Иначе topViewController будет неактуальным.
Напоследок реализуем методы UINavigationController, которые изменяют навигационный стек, через updateNavigationStack(animated:applyChanges:).
Методы, манипулирующие стеком UINavigationController
Запустим и посмотрим, что получилось.
Стало лучше и размер контента учитывается, но с артефактами. Посмотрим на iOS 12.
Транзишен отличается в худшую сторону и после окончания размер контента остаётся с предыдущего view controller.
Получается, мы не можем рассчитывать на системный транзишен UINavigationController, и нам нужно реализовать его своими силами. Реализуем UINavigationControllerDelegate, в котором переопределим транзишен для push и pop операций.
Транзишен для push и pop операций
Далее реализуем UINavigationControllerDelegate и переопределим анимацию транзишена.
Запустим приложение и последим за анимациями.
Всё работает плавно, как ожидается!
Интерактивный pop
Когда мы переопределили транзишен для push и pop операций, то мы потеряли системный интерактивный переход, который до этого работал из коробки. Придется реализовать его самим. Используем navigationController(_:interactionControllerFor:) из UINavigationControllerDelegate. Через него можно добавить интерактивный транзишен при переходе между view controller’ами.
Но как повторить жест, который срабатывает при свайпе от края экрана? Воспользуемся UIScreenEdgePanGestureRecognizer, который делает ровно то, что нам нужно.
Будем добавлять этот жест для view controller’а, который запушился в иерархию navigation controller’а. Привожу только критические участки кода.
В обработчике жеста делаем то же самое, что и для pan gesture. Инициируем интерактивный транзишен, обновляем прогресс пропорционально движению пальца и используем тот же критерий для отмены и завершения перехода.
Остаётся прокачать UINavigationControllerDelegate и учесть в нём интерактивный транзишен. На push добавляем к view controller’у жест для интерактивного pop’а.
И ура, мы справились с нашим последним вызовом!
Что мы сделали в третьей части?
Адаптировали UINavigationController под размер контента.
Добились корректных push и pop транзишенов на iOS 12+.
Поддержали уникальный интерактивный pop.
Заключение
Спасибо, что дочитали до конца! Теперь можно и резюме обновить.
Мы начали с нуля, а в конце получили многофункциональный Bottom Sheet. Ответили на вопросы, которые стояли в начале статьи, и даже вспомнили школьную физику. Надеюсь, что статья оказалась полезной, и в вашем приложении Bottom Sheet заиграет новыми красками!