Как сделать цветные тени в Android с градиентом и анимацией

На презентации новых макбуков и обратил внимание на картинку процессора:

Переливающиеся цветные тени на темном фоне, выглядит классно.

Вот дошли руки, решил попробовать нарисовать на андроиде так же. Вот что получилось:

Сразу оговорюсь, что стандартным способом это сделать нельзя, до api 28 есть поддержка только черных elevation, после api 28 добавили поддержку цветных теней, но градиент сделать не получится. Поэтому мы будет рисовать drawable, устанавливать его в виде background и применять padding на целевой вьюхе, чтобы контент был внутри тени.

Напишем функцию создания Drawable с тенью:

/**  * Создание drawable с градиентом-тенью  */ private fun createShadowDrawable(     @ColorInt colors: IntArray,     cornerRadius: Float,     elevation: Float,     centerX: Float,     centerY: Float ): ShapeDrawable {      val shadowDrawable = ShapeDrawable()      // Устанавливаем черную тень по умолчанию     shadowDrawable.paint.setShadowLayer(         elevation, // размер тени         0f, // смещение тени по оси Х         0f, // по У         Color.BLACK // цвет тени     )      /**      * Применяем покраску градиентом      *      * @param centerX - Центр SweepGradient по оси Х. Берем центр вьюхи      * @param centerY - Центр по оси У      * @param colors - Цвета градиента. Последний цвет должен быть равен первому,      * иначе между ними не будет плавного перехода      * @param position - позиции смещения градиента одного цвета относительно другого от 0 до 1.      * В нашем случае null т.к. нам нужен равномерный градиент      */     shadowDrawable.paint.shader = SweepGradient(         centerX,         centerY,         colors,         null     )      // Делаем закугление углов     val outerRadius = FloatArray(8) { cornerRadius }     shadowDrawable.shape = RoundRectShape(outerRadius, null, null)      return shadowDrawable }

Поскольку у этого drawable фон представлен в виде радуги тех цветов, что мы передали в параметрах, нам нужен нормальный одноцветный фон. Для этого создаем вторую drawable:

/**  * Создание цветного drawable с закругленными углами  * Это будет основной цвет нашего контейнера  */ private fun createColorDrawable(     @ColorInt backgroundColor: Int,     cornerRadius: Float ) = GradientDrawable().apply {         setColor(backgroundColor)         setCornerRadius(cornerRadius)     }

Функция установки бэкграунда на вьюху-контейнер. У нас будет LayerDrawable с двумя слоями. 1 — тень, 2 — просто цвет с закругленными углами.

/**  * Устанавливаем бэкграунд с тенью на вьюху, учитывая padding  */ private fun View.setColorShadowBackground(     shadowDrawable: ShapeDrawable,     colorDrawable: Drawable,     padding: Int ) {     val drawable = LayerDrawable(arrayOf(shadowDrawable, colorDrawable))     drawable.setLayerInset(0, padding, padding, padding, padding)     drawable.setLayerInset(1, padding, padding, padding, padding)     setPadding(padding, padding, padding, padding)     background = drawable }

Применяем на вьюхе:

// ждем когда вьюха отрисуется чтобы узнать ее размеры targetView.doOnNextLayout {     val colors = intArrayOf(         Color.WHITE,         Color.RED,         Color.WHITE     )     val cornerRadius = 16f.dp     val padding = 30.dp     val centerX = it.width.toFloat() / 2 - padding     val centerY = it.height.toFloat() / 2 - padding      val shadowDrawable = createShadowDrawable(         colors = colors,         cornerRadius = cornerRadius,         elevation = padding / 2f,         centerX = centerX,         centerY = centerY     )     val colorDrawable = createColorDrawable(         backgroundColor = Color.DKGRAY,         cornerRadius = cornerRadius     )      it.setColorShadowBackground(         shadowDrawable = shadowDrawable,         colorDrawable = colorDrawable,         padding = 30.dp     ) }

Теперь проанимируем изменение с одного набора цветов на другие. Зациклим.

/**  * Анимация drawable-градиента  */ private fun animateShadow(     shapeDrawable: ShapeDrawable,     @ColorInt startColors: IntArray,     @ColorInt endColors: IntArray,     duration: Long,     centerX: Float,     centerY: Float ) {     /**      * Меняем значение с 0f до 1f для применения плавного изменения      * цвета с помощью [ColorUtils.blendARGB]      */     ValueAnimator.ofFloat(0f, 1f).apply {         // Задержка перерисовки тени. Грубо говоря, фпс анимации         val invalidateDelay = 100         var deltaTime = System.currentTimeMillis()          // Новый массив со смешанными цветами         val mixedColors = IntArray(startColors.size)          addUpdateListener { animation ->             if (System.currentTimeMillis() - deltaTime > invalidateDelay) {                 val animatedFraction = animation.animatedValue as Float                 deltaTime = System.currentTimeMillis()                  // Смешиваем цвета                 for (i in 0..mixedColors.lastIndex) {                     mixedColors[i] = ColorUtils.blendARGB(startColors[i], endColors[i], animatedFraction)                 }                  // Устанавливаем новую тень                 shapeDrawable.paint.shader = SweepGradient(                     centerX,                     centerY,                     mixedColors,                     null                 )                 shapeDrawable.invalidateSelf()             }         }         repeatMode = ValueAnimator.REVERSE         repeatCount = Animation.INFINITE         setDuration(duration)         start()     } }

Применим:

// Второй массив с цветами. Размер массивов должен быть одинаковый. val endColors = intArrayOf( 	Color.RED, 	Color.WHITE, 	Color.RED ) animateShadow( 	shapeDrawable = shadowDrawable,   startColors = colors,   endColors = endColors,   duration = 2000,   centerX = centerX,   centerY = centerY )

Все. Если это будет кнопкой, нужно применить ripple эффект для foreground вьюхи и так же прописать там отступ, чтобы у нас отображалась анимация нажатия.

ссылка на оригинал статьи https://habr.com/ru/post/530376/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *