16 сентября в России стартовал фильм по мотивам романа Фрэнка Герберта «Дюна». Команда маркетинга Додо Пиццы часто проводит коллаборации по самым разным поводам и с самыми разными компаниями. «Дюна» не стала исключением — в продаже появилось комбо Дюна в специальной коробке и маска AR в Instagram. Обычно промоушен в приложении сводится к замене картинок в меню и добавлению рекламного баннера, но на этот раз разработчики решили не оставаться в стороне и немножко покреативить. Команда мобильной разработки захотела привлечь больше внимания к новому комбо «Дюна» в приложении. Задача осложнялась тем, что нужно было всё сделать не просто быстро, а супербыстро из-за переноса премьеры на месяц раньше. Основную идею предложил iOS-разработчик нашей команды Алексей Берёзка, а весь процесс реализации он описал в посте в Twitter. Всё, что оставалось для Android — «сделать, как на iOS» (кажется, эта шутка никогда не надоест… или это уже не шутка). Привлекать внимание к комбо «Дюна» мы решили в двух местах — в меню и в карточке комбо. По умолчанию они выглядят так: В меню решили выделить комбо на фоне других продуктов. «Дюна» ассоциируется с планетой Арракис, поэтому изображение пустыни очень хорошо подходит: Хотим сделать карточку продукта красивее. Самое простое — добавить фон. Получилось бы примерно так: Но Берёзке пришла идея лучше — оживить карточку с помощью движения изображений. Проследить за развитием этой мысли можно в его посте. Обсудили и пришли к тому, что очень круто было бы заставить пиццу летать вместе со стикерами и пуншем в ответ на поворот устройства в пространстве. А ещё круче, если продукты будут двигаться с разной скоростью, создавая у наблюдателя эффект параллакса. Маркетинг был в восторге от первого прототипа этой идеи. Решили делать именно так. У iOS есть UIInterpolatingMotionEffect, позволяющий добиться эффекта параллакса в одну строку. Для Android не удалось найти подходящего коробочного решения, поэтому пришлось стряхнуть пыль с медали за достижения в велосипедном спорте и сделать всё практически с нуля. На решение вдохновила библиотека Parallax Layer Layout, однако она нам не очень подходила — пришлось бы использовать view group из неё, что потребовало бы больше изменений, чем мы хотели, а в будущем снизило бы гибкость разметки. В идеальном случае хочется уметь также в одну строку задавать анимацию при повороте устройства, дополнительно указывая максимальный сдвиг view относительно начального положения: Как это реализовать? Идея очень простая: нам нужно получить изменение угла поворота устройства вокруг осей. Попробуем сделать. Android предоставляет множество сенсоров, сообщающих об изменении положения устройства в пространстве. Для определения наклона устройства можно использовать сенсоры GYROSCOPE и ROTATION VECTOR. Второй сенсор, rotation vector, является виртуальным и совмещает в себе показания нескольких физических сенсоров устройства (в том числе и гироскопа). Над этими показаниями выполняются некоторые вычисления и мы получаем уже преобразованный результат. Данный сенсор чуть удобнее, чем гироскоп, потому что нужно будет выполнить меньше вычислений вручную. Для начала получим сенсор: Далее нужно зарегистрировать слушателя изменений сенсора: Первый параметр — слушатель изменений сенсора. Мы реализуем его дальше. Второй — непосредственно сенсор, изменения которого нужно слушать. Третий параметр довольно загадочный — это желаемый период времени между событиями сенсора в микросекундах. Загадочный он потому, что система вовсе не обязана соблюдать указанное количество времени между событиями, и об этом явно говорится в документации. Для себя мы установили этот период в 100 000 микросекунд, так как при этом события приходили от сенсора с примерно устраивающим нас периодом. Напишем реализацию метода onSensorChanged. Класс SensorManager содержит вспомогательные методы для выполнения рутинной математики в трёхмерном пространстве. Среди прочих есть метод getAngleChange. С его помощью мы как раз можем получить изменение угла наклона устройства в пространстве между двумя положениями. Начальное значение нам придётся запомнить, когда придёт первое событие от сенсора. Далее будем считать изменение угла относительно этого положения. Метод getAngleChange принимает на вход начальную и конечную матрицу поворота, а ROTATION_VECTOR сообщает вектор поворота. Для преобразования одного в другое в SensorManager есть метод getRotationMatrixFromVector. Свяжем всё вместе и получим первую реализацию: Теперь в массиве angleChange хранится изменение угла поворота в радианах в диапазоне от -π до +π. Элемент с индексом 0 — поворот относительно оси Z, 1 — относительно оси X и 2 — относительно Y. Чтобы было удобнее, преобразуем радианы в число от -1 до 1: Теперь ничто не мешает нам изменить положение view на экране: Выглядит это примерно так: Движение не очень плавное, что будет ещё более заметно, если быстро вращать устройство: Чтобы сделать движение более плавным, добавим анимацию: Вот что получим: Уже выглядит значительно лучше! Обратите внимание на пару нюансов: если хранить view напрямую, будет происходить утечка памяти. Также нужно не забывать отписываться от событий сенсора. Проще всего подчистить всё, подписавшись на lifecycle с помощью androidx.lifecycle; на некоторых устройствах Samsung метод getRotationMatrixFromVector бросает исключение при длине вектора поворота больше 4. Решение: обрезать вектор поворота по длине (это делается в методе getRotationVectorFromSensorEvent). Вся интеграция класса в приложение теперь занимает 3 строки (по одной на пиццу, пунш и стикеры): Значения максимального смещения 26, 50 и 108 были подобраны дизайнером. В итоге получаем более живую карточку комбо: Демо проект можно найти вот тут на GitHub. А комбо «Дюна» можно заказать в приложении до 31 октября. ;-) Приятного аппетита! Разработчик