Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Когда заходит речь про тени на Android, возникает сразу несколько вопросов. Первый: зачем они нужны? Второй: почему нельзя использовать системные тени и жить счастливо? Третий: если нельзя использовать системные тени, как реализовать кастомные?
Это Сергей Петров, Android-разработчик в команде Design System inDrive, и вместе мы поговорим о тенях на Android.
На второй вопрос вам ответят дизайнеры. Возможно, вы попытаетесь убедить их в том, что системные тени мы получаем почти бесплатно, с минимальными трудозатратами и максимальной производительностью. Вероятно, вы приведете в качестве аргумента соответствие гайдлайнам Material.
Искренне надеюсь, что ваша стойкость и убедительность позволит вам и дальше использовать elevation
для отрисовки теней. Если нет — придется искать ответ на третий вопрос.
Оговорюсь, что изначально я пробовал подобрать нужные значения параметров для системных теней. В Android, начиная с API 21, доступны атрибуты темы ambientShadowAlpha и spotShadowAlpha. С помощью них можно регулировать глобальные настройки прозрачности теней.
А позже в API 28 добавили возможность настраивать цвета теней через атрибуты темы outlineAmbientShadowColor и outlineSpotShadowColor, а также свойства View
— outlineAmbientShadowColor и outlineSpotShadowColor.
Elevation
Попробуем подобрать подходящий elevation
и прозрачность тени, и посмотрим, что из этого получится.
У нас в дизайне есть три разновидности теней (представлены на картинке ниже):
S — размер 12dp.
M — размер 20dp.
L — размер 32dp.
У каждой тени свои настройки прозрачности и смещения по оси Y. На смещение мы влиять не можем, но хотя бы попробуем подобрать значения прозрачности. Сложность в том, что до API 28 эти значения глобальны в рамках темы. Задать разным по стилю теням разные прозрачности, как в дизайне, возможности нет. К тому же, цвет тени в дизайне не черный, как в дефолтном Android. Что ж, попробуем добиться хотя бы примерного сходства.
Долго и усердно подбираем значения, примерно подходящие всем трем теням сразу.
// тема
<item name="android:ambientShadowAlpha">0.01</item>
<item name="android:spotShadowAlpha">0.08</item>
// настройки elevatiom
<dimen name="elevation_s">12dp</dimen>
<dimen name="elevation_m">24dp</dimen>
<dimen name="elevation_l">30dp</dimen>
Кажется, получается довольно неплохо. Тень S слегка отличается, но две другие выглядят сносно. Настроить точнее при помощи общих настроек прозрачности вряд ли получится, но, начиная с API 28, можно получить совсем точное совпадение.
Получается, задача решена, и можно убеждать дизайнера, что тень S будет слегка отличаться от дизайна. В конце концов, пользователь вряд ли это заметит, да и кто вообще из пользователей Android обращает внимание на такие мелочи.
Но оказалось, что не все так просто. В Android два источника света: ambient light, который светит во все стороны, и key light — светит направленно. Кому интересно, в этой статье очень хорошо и с картинками раскрыта эта тема.
Тот, что светит сверху под углом, и есть key light. Он дает ярко выраженную тень в нижней части объекта. И вот, что происходит с тенью, особенно при больших elevation
, по мере отдаления от верхней части экрана.
Как в жизни: чем дальше от источника света, тем длиннее тень. Можно ли на это повлиять? В данной статье в разделе Don’t try this at home утверждается, что да, но у меня не получилось. Но даже если бы и получилось, и на код-ревью закрыли глаза на этот очевидный хак, это не решило проблемы полностью. Где бы не размещался источник света, тени в любом случае были бы неравномерными. Причина тому — большой elevation
, необходимый для достижения нужного эффекта.
Изрядно расстроившись, переходим к плану Б — рисовать тень самостоятельно.
MaterialShapeDrawable
Раз не получилось с elevation
, попробуем другой бесплатный метод. Вспоминаем, что в Material библиотека имеет поддержку теней и на античных устройствах. Давайте посмотрим на реализацию.
Заглядываем внутрь MaterialShapeDrawable и видим, что они на пару с неким ShadowRenderer занимаются интересными вещами. По заданным параметрам формы тень отрисовывается при помощи шейдеров LinearGradient и RadialGradient. То есть, тень — это градиент вокруг формы.
Идея интересная, попробуем ее в действии. Для этого сделаем простую кастомную вьюшку и посмотрим, что получится.
val shape = ShapeAppearanceModel.builder()
.setAllCornerSizes(16.toPx())
.build()
val drawable = MaterialShapeDrawable(shape)
drawable.fillColor = ColorStateList.valueOf(Color.WHITE)
drawable.shadowVerticalOffset = 8.toPx()
drawable.shadowRadius = 32.toPx()
drawable.shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
background = drawable
Кажется, работает, но есть пару негативных моментов. Во-первых, настройки прозрачности градиента зашиты в ShadowRenderer. Чтобы сделать тень по своим параметрам, придется собирать код, разбросанный по нескольким классам, и копировать в проект.
Во-вторых, производительность решения оставляет желать лучшего. Ради интереса решено было повесить на вьюшку аниматор, который менял ее размер, и посмотреть, как будет работать отрисовка. Лаги, даже на релизной сборке, были заметны невооруженным глазом, что подтвердил и systrace
.
Время отрисовки одного кадра — 18 миллисекунд. Это только draw
одной вьюшки на экране. А draw
— довольно частая операция