From 4562e8e2dc9294c7f2dce52004bdc558616287f2 Mon Sep 17 00:00:00 2001 From: Lucas Dupin Date: Tue, 5 May 2020 01:21:46 -0700 Subject: [PATCH] Add new touch effect to player Test: manual Bug: 155796337 Change-Id: I6e0c20522ee99c99d0b1a9f77376c5ca9838703b --- .../res/drawable/qs_media_background.xml | 16 +- packages/SystemUI/res/values/attrs.xml | 7 + .../systemui/media/IlluminationDrawable.kt | 190 ++++++++++++++++++ .../android/systemui/qs/QSMediaPlayer.java | 13 +- 4 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index 2821e4c28bab8..e79c9a40918c5 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -14,13 +14,9 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> - - - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index d3256ef34bd76..c4195940d11a3 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -154,5 +154,12 @@ + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt new file mode 100644 index 0000000000000..7da991610fedb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt @@ -0,0 +1,190 @@ +package com.android.systemui.media + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.res.ColorStateList +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.RadialGradient +import android.graphics.Shader +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.MathUtils +import android.util.MathUtils.lerp +import androidx.annotation.Keep +import com.android.internal.graphics.ColorUtils +import com.android.internal.graphics.ColorUtils.blendARGB +import com.android.systemui.Interpolators +import com.android.systemui.R +import org.xmlpull.v1.XmlPullParser + +private const val BACKGROUND_ANIM_DURATION = 370L +private const val RIPPLE_ANIM_DURATION = 800L +private val GRADIENT_STOPS = floatArrayOf(0.2f, 1f) + +private data class RippleData( + var x: Float, + var y: Float, + var alpha: Float, + var progress: Float, + var minSize: Float, + var maxSize: Float, + var highlight: Float +) + +/** + * Drawable that can draw an animated gradient when tapped. + */ +@Keep +class IlluminationDrawable : Drawable() { + + private var cornerRadius = 0f + private var highlightColor = Color.TRANSPARENT + private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f) + private var tmpHsl = floatArrayOf(0f, 0f, 0f) + private var paint = Paint() + + var backgroundColor = Color.TRANSPARENT + set(value) { + if (value == field) { + return + } + field = value + animateBackground() + } + + private var rippleAnimation: AnimatorSet? = null + private var backgroundAnimation: ValueAnimator? = null + + /** + * Draw background and gradient. + */ + override fun draw(canvas: Canvas) { + paint.shader = if (rippleData.progress > 0) { + val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) + val centerColor = blendARGB(paint.color, highlightColor, rippleData.alpha) + RadialGradient(rippleData.x, rippleData.y, radius, intArrayOf(centerColor, paint.color), + GRADIENT_STOPS, Shader.TileMode.CLAMP) + } else { + null + } + canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), + cornerRadius, cornerRadius, paint) + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSPARENT + } + + override fun inflate( + r: Resources, + parser: XmlPullParser, + attrs: AttributeSet, + theme: Resources.Theme? + ) { + val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable) + cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) + rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) + rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) + rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f + a.recycle() + } + + override fun setColorFilter(p0: ColorFilter?) { + throw UnsupportedOperationException("Color filters are not supported") + } + + override fun setAlpha(value: Int) { + throw UnsupportedOperationException("Alpha is not supported") + } + + /** + * Cross fade background. + * @see setTintList + * @see backgroundColor + */ + private fun animateBackground() { + ColorUtils.colorToHSL(backgroundColor, tmpHsl) + val L = tmpHsl[2] + tmpHsl[2] = MathUtils.constrain(if (L < 1f - rippleData.highlight) { + L + rippleData.highlight + } else { + L - rippleData.highlight + }, 0f, 1f) + + val initialBackground = paint.color + val initialHighlight = highlightColor + val finalHighlight = ColorUtils.HSLToColor(tmpHsl) + + backgroundAnimation?.cancel() + backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply { + duration = BACKGROUND_ANIM_DURATION + interpolator = Interpolators.FAST_OUT_LINEAR_IN + addUpdateListener { + val progress = it.animatedValue as Float + paint.color = blendARGB(initialBackground, backgroundColor, progress) + highlightColor = blendARGB(initialHighlight, finalHighlight, progress) + invalidateSelf() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + backgroundAnimation = null + } + }) + start() + } + } + + override fun setTintList(tint: ColorStateList?) { + super.setTintList(tint) + backgroundColor = tint!!.defaultColor + } + + /** + * Draws an animated ripple that expands from {@param x} and {@param y} fading away. + * + * @param x X position of gradient centroid. + * @param y Y position of gradient centroid. + */ + fun illuminate(x: Int, y: Int) { + rippleData.x = x.toFloat() + rippleData.y = y.toFloat() + rippleData.alpha = 1f + rippleData.progress = 0f + invalidateSelf() + + rippleAnimation?.cancel() + rippleAnimation = AnimatorSet().apply { + playTogether(ValueAnimator.ofFloat(1f, 0f).apply { + startDelay = 133 + duration = RIPPLE_ANIM_DURATION - startDelay + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.alpha = it.animatedValue as Float + invalidateSelf() + } + }, ValueAnimator.ofFloat(0f, 1f).apply { + duration = RIPPLE_ANIM_DURATION + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.progress = it.animatedValue as Float + invalidateSelf() + } + }) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + rippleData.progress = 0f + rippleAnimation = null + invalidateSelf() + } + }) + start() + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java index e76cd51168189..0792f4dd18ff8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -22,6 +22,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.MediaDescription; @@ -38,6 +39,7 @@ import android.widget.TextView; import com.android.settingslib.media.LocalMediaManager; import com.android.systemui.R; +import com.android.systemui.media.IlluminationDrawable; import com.android.systemui.media.MediaControlPanel; import com.android.systemui.media.SeekBarObserver; import com.android.systemui.media.SeekBarViewModel; @@ -173,7 +175,7 @@ public class QSMediaPlayer extends MediaControlPanel { LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; int i = 0; for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + final ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); if (thatBtn == null || thatBtn.getDrawable() == null || thatBtn.getVisibility() != View.VISIBLE) { @@ -187,6 +189,15 @@ public class QSMediaPlayer extends MediaControlPanel { thisBtn.setOnClickListener(v -> { Log.d(TAG, "clicking on other button"); thatBtn.performClick(); + if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { + Rect mediaRect = new Rect(); + Rect buttonRect = new Rect(); + mMediaNotifView.getGlobalVisibleRect(mediaRect); + thisBtn.getGlobalVisibleRect(buttonRect); + ((IlluminationDrawable) mMediaNotifView.getBackground()).illuminate( + buttonRect.centerX() - mediaRect.left, + buttonRect.centerY() - mediaRect.top); + } }); }