Как стать автором
Обновить
197.67
2ГИС
Главные по городской навигации

Хакнуть Qt: как мы запускали Picture-in-Picture в навигаторе 2ГИС

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров4K

Привет! Я Егор Ерусланов, Qt-разработчик.

В Android-приложение 2ГИС мы добавили новый режим PiP (Picture-in-Picture, или «картинка в картинке»). С PiP наши пользователи смогут следить за маршрутом в маленьком плавающем окне на основном экране. Например, когда нужно быстро прочитать сообщение или включить подкаст и при этом сохранять фокус на навигации. Режим PiP — это не просто «приятная мелочь», а функциональность, которая подстраивается под новые требования пользователей навигатора. 

В этом посте поделюсь, как решал несколько трудностей, которые встретились при разработке этой функции.

Зачем нужен PiP?

С PiP мы решаем несколько пользовательских ожиданий ↓

  1. Многозадачность: пользователи могут свернуть приложение и одновременно пользоваться другими функциями смартфона. Это подтверждается нашим внутренним исследованием: около 36% пользователей навигатора сворачивают 2ГИС в фоновый режим минимум на три минуты, чтобы переключиться на другие приложения. 

  2. Минимализм: компактное окно предоставляет только самую необходимую информацию — манёвры, скорость и текущие ограничения.

  3. Новаторство в пользовательских привычках: многие популярные приложения, такие как YouTube или Google Maps, уже поддерживают PiP, и пользователи могут ожидать увидеть эту функциональность в других приложениях. Ещё не так много российских компаний реализовали эту фичу, а мы решили поддержать, так как любим радовать пользователей, которые ценят новые технологии.

Старт разработки и первые шаги

Всё как и обычно началось с запроса продактов и дизайнеров. Мне показали пару скриншотов и макет, как это может элегантно выглядеть в приложении. Задача: сделать функциональность, которая будет плавной и красивой. На первом этапе я сделал поверхностный ресёрч и составил первую оценку. На первый взгляд всё выглядело просто: Android предоставляет готовое API для работы с PiP, и нужно было лишь использовать его.

Как же я ошибался 😅 

Про сложности

Уже во время разработки стало понятно, что задача потребует больше времени.

1. Адаптация карты

Помимо интерфейса 2ГИС (UI на QML поверх карты) пришлось адаптировать также карту, так как нам не подходил масштаб и зум, которые брались по умолчанию. Мы продумывали несколько вариантов решения: 

  • Там, где необходимо использовать особую стилизацию карты, её масштаб и зумы, мы обычно заводим новый тип карты, таблицу зумов и стили. Так, например, мы делаем в нашей мини-карте и карте в Cluster-дисплее CarPlay. 

  • Используем нашу основную карту, но изменять её масштаб в рантайме.

Первый вариант показался слишком дорогим, а мы хотели максимально сэкономить ресурсы разработки. Поэтому пошли по второму пути: заскейлили карту до 70% от её оригинального размера в момент перехода в режим PiP и сбрасывали коэффициент масштабирования (scale factor) при возвращении в приложение. По итогу мы использовали существующий функционал нашей карты, который позволял всё сделать без дополнительных доработок.

2. Неподдерживаемый PiP в Qt

Так как наше приложение разработано на C++/Qt/QML, первым делом возник вопрос, а поддерживает ли Qt 5.15 вообще такую функциональность Android, как режим Picture-in-Picture (PiP). Если коротко — нет. Классы-обёртки, которые предоставляет Qt для работы с Android Activity, не имеют встроенной поддержки PiP. 

Однако класс QtActivity, используемый в качестве главного Activity Android-приложения на Qt, расширяет стандартный класс Activity, и можно вручную вызвать setPictureInPictureParams, перегрузить метод onPictureInPictureModeChanged и настроить отображение карты, а также интерфейс в QML. 

На первый взгляд проблема решена. 

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

Жизненный цикл Android Activity и его связь с PiP

Чтобы разобраться в сути проблемы, начнём с рассмотрения жизненного цикла Android Activity и места, которое в нём занимает режим PiP. Согласно документации, при переходе в режим Picture-in-Picture система вызывает метод onPause(). Это означает, что в классической реализации Activity в режиме PiP находится в состоянии Paused и далее живёт в соответствии с жизненным циклом Activity.

Жизненный цикл Activity
Жизненный цикл Activity

Состояния Qt-приложения

В свою очередь, у приложений на Qt есть собственный механизм управления состояниями, который накладывает определённые ограничения на работу компонентов Qt. Вот основные состояния приложения Qt, которые я сопоставил с состояниями в жизненном цикле Android Activity ↓

Соотношение состояний Qt-приложения и жизненного цикла Activity
Соотношение состояний Qt-приложения и жизненного цикла Activity

По этой схеме:

  1. Вызов onResume() переводит состояние Qt-приложения в ApplicationActive.

  2. Вызов onPause() переводит состояние Qt-приложения в ApplicationInactive.

  3. Вызов onStop() переводит состояние Qt-приложения в ApplicationHidden или ApplicationSuspended (в зависимости от того, выполняется ли фоновая работа).

Итак, при переходе в режим PiP вызывается метод onPause(), и Qt переходит в состояние ApplicationInactive. Фреймворк обрабатывает вызов onPause() в своём делегате QtActivityDelegate.

При переходе в каждое из состояний Qt также накладывает определённые внутренние ограничения на работу циклов обработки событий. Например, выставляет флаги, из-за которых при вызове метода processEvents() перестают обрабатываться некоторые типы событий. При этом Qt не отбрасывает эти события, а заполняет ими очередь, которая будет обработана позже — когда processEvents() вызовется без соответствующих ограничивающих флагов.

В нашем случае при переходе в состояние ApplicationInactive Qt останавливает обработку некоторых событий, устанавливая ограничение через флаг X11ExcludeTimers. Этот флаг не задокументирован в Qt, но можно предположить, как он влияет на обновление QML. Например, анимации в QML основаны на таймерах, а значит, при выставлении этого флага события таймеров перестают обрабатываться.

Анализ показал, что ограничения, которые Qt накладывает на обработку событий при переходе в состояние ApplicationInactive, нам не подходят, так как именно они приводят к некорректному обновлению UI.

Решение проблемы

Всё оказалось довольно просто, хотя и грубовато. Мы использовали публичный метод QtNative, который позволяет вручную перевести приложение Qt в состояние ApplicationActive после перехода в режим PiP. Теперь, при переходе состояния Android Activity в Paused, мы переключаем внутреннее состояние Qt-приложения в ApplicationActive по условию нахождения в режиме PiP. Это позволяет приложению продолжать обрабатывать события так, как будто находится в обычном активном состоянии.

Схема финального решения
Схема финального решения

После применения такого подхода интерфейс в QML начал корректно обновляться даже в режиме Picture-in-Picture.

Как выглядит наш PiP

При переходе в PiP-режим мы полностью пересоздаём нашу страницу навигатора, при этом масштабируем её до нужного нам размера и убираем лишние части функционала. Например, поп-апы, плашки о лучшем маршруте, дашборд и всё остальное. Чтобы интерфейс выглядел минималистично, мы оставили: 

  • карту с маршрутом,

  • плашку манёвров,

  • маркер ведения,

  • спидометр.

Картинка в картинке во время ведения по маршруту для автомобиля (1) и пешехода (2). Чтобы вернуться в 2ГИС, нужно нажать на иконку с рамкой [ ], закрыть окно режима «Картинка в картинке» — на × (3)
Картинка в картинке во время ведения по маршруту для автомобиля (1) и пешехода (2). Чтобы вернуться в 2ГИС, нужно нажать на иконку с рамкой [ ], закрыть окно режима «Картинка в картинке» — на × (3)

Мы долго экспериментировали с другими элементами, такими как график высот или индикатор пробок, но пришли к выводу, что это только перегружает и так маленькое окно.

Итог

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

Я был уверен, что выпущенные в 2020 году Qt 5.15, который мы используем, уже поддерживает режим PiP, ведь эта возможность появилась в Android 8 ещё в 2017 году. Однако оказалось, что это не так.

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

P.S. У нас открыта вакансия для С++/Qt/QML-разработчиков и для ребят, кто знает Qt. Посмотри, вдруг тебе тоже захочется развивать 2ГИС с нами! Или подпишись на телеграм-бота: подходящая вакансия сама постучится к тебе в личку.

Теги:
Хабы:
+26
Комментарии14

Публикации

Информация

Сайт
2gis.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Наталья Акберова

Истории