diff options
9 files changed, 356 insertions, 161 deletions
diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index 80db3bec86c1..656d2e4a33ff 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -16,7 +16,5 @@ --> <com.android.systemui.media.IlluminationDrawable xmlns:systemui="http://schemas.android.com/apk/res-auto" - systemui:rippleMinSize="30dp" - systemui:rippleMaxSize="135dp" systemui:highlight="15" systemui:cornerRadius="?android:attr/dialogCornerRadius" />
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_media_light_source.xml b/packages/SystemUI/res/drawable/qs_media_light_source.xml new file mode 100644 index 000000000000..b2647c1f6697 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_media_light_source.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.media.LightSourceDrawable + xmlns:systemui="http://schemas.android.com/apk/res-auto" + systemui:rippleMinSize="25dp" + systemui:rippleMaxSize="135dp" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml index 1a1fddbcfd03..d721818d6e54 100644 --- a/packages/SystemUI/res/layout/media_view.xml +++ b/packages/SystemUI/res/layout/media_view.xml @@ -94,7 +94,8 @@ android:id="@+id/media_seamless" android:layout_width="0dp" android:layout_height="wrap_content" - android:background="@*android:drawable/media_seamless_background" + android:foreground="@*android:drawable/media_seamless_background" + android:background="@drawable/qs_media_light_source" android:orientation="horizontal" android:forceHasOverlappingRendering="false" android:paddingLeft="12dp" diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 81020432daa7..b8071699830e 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -622,7 +622,7 @@ </style> <style name="MediaPlayer.Button" parent="@android:style/Widget.Material.Button.Borderless.Small"> - <item name="android:background">@null</item> + <item name="android:background">@drawable/qs_media_light_source</item> <item name="android:tint">@android:color/white</item> <item name="android:stateListAnimator">@anim/media_button_state_list_animator</item> </style> diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt index 743216556434..10b36e9eb85a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt @@ -1,8 +1,23 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 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 @@ -10,16 +25,12 @@ import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter +import android.graphics.Outline import android.graphics.Paint import android.graphics.PixelFormat -import android.graphics.RadialGradient -import android.graphics.Rect -import android.graphics.Shader import android.graphics.drawable.Drawable import android.util.AttributeSet import android.util.MathUtils -import android.util.MathUtils.lerp -import android.view.MotionEvent import android.view.View import androidx.annotation.Keep import com.android.internal.graphics.ColorUtils @@ -29,20 +40,6 @@ 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 const val RIPPLE_DOWN_PROGRESS = 0.05f -private const val RIPPLE_CANCEL_DURATION = 200L -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. @@ -53,9 +50,10 @@ class IlluminationDrawable : Drawable() { private var themeAttrs: IntArray? = null 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() + private var highlight = 0f + private val lightSources = arrayListOf<LightSourceDrawable>() private var backgroundColor = Color.TRANSPARENT set(value) { @@ -66,70 +64,20 @@ class IlluminationDrawable : Drawable() { animateBackground() } - /** - * Draw a small highlight under the finger before expanding (or cancelling) it. - */ - private var pressed: Boolean = false - set(value) { - if (value == field) { - return - } - field = value - - if (value) { - rippleAnimation?.cancel() - rippleData.alpha = 1f - rippleData.progress = RIPPLE_DOWN_PROGRESS - } else { - rippleAnimation?.cancel() - rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { - duration = RIPPLE_CANCEL_DURATION - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.alpha = it.animatedValue as Float - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - var cancelled = false - override fun onAnimationCancel(animation: Animator?) { - cancelled = true; - } - - override fun onAnimationEnd(animation: Animator?) { - if (cancelled) { - return - } - rippleData.progress = 0f - rippleData.alpha = 0f - rippleAnimation = null - invalidateSelf() - } - }) - start() - } - } - invalidateSelf() - } - - private var rippleAnimation: Animator? = 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 getOutline(outline: Outline) { + outline.setRoundRect(bounds, cornerRadius) + } + override fun getOpacity(): Int { return PixelFormat.TRANSPARENT } @@ -151,14 +99,8 @@ class IlluminationDrawable : Drawable() { cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) } - if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) { - rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) - } - if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) { - rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) - } if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { - rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / + highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f } } @@ -192,10 +134,10 @@ class IlluminationDrawable : Drawable() { private fun animateBackground() { ColorUtils.colorToHSL(backgroundColor, tmpHsl) val L = tmpHsl[2] - tmpHsl[2] = MathUtils.constrain(if (L < 1f - rippleData.highlight) { - L + rippleData.highlight + tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) { + L + highlight } else { - L - rippleData.highlight + L - highlight }, 0f, 1f) val initialBackground = paint.color @@ -210,6 +152,7 @@ class IlluminationDrawable : Drawable() { val progress = it.animatedValue as Float paint.color = blendARGB(initialBackground, backgroundColor, progress) highlightColor = blendARGB(initialHighlight, finalHighlight, progress) + lightSources.forEach { it.highlightColor = highlightColor } invalidateSelf() } addListener(object : AnimatorListenerAdapter() { @@ -226,69 +169,11 @@ class IlluminationDrawable : Drawable() { backgroundColor = tint!!.defaultColor } - /** - * Draws an animated ripple that expands fading away. - */ - private fun illuminate() { - rippleData.alpha = 1f - 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(rippleData.progress, 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() - } - } - - /** - * Setup touch events on a view such as tapping it would trigger effects on this drawable. - * @param target View receiving touched. - * @param container View that holds this drawable. - */ - fun setupTouch(target: View, container: View) { - val containerRect = Rect() - target.setOnTouchListener { view: View, event: MotionEvent -> - container.getGlobalVisibleRect(containerRect) - rippleData.x = event.rawX - containerRect.left - rippleData.y = event.rawY - containerRect.top - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - pressed = true - } - MotionEvent.ACTION_MOVE -> { - invalidateSelf() - } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - pressed = false - if (event.action == MotionEvent.ACTION_UP) { - illuminate() - } - } - } - false + fun registerLightSource(lightSource: View) { + if (lightSource.background is LightSourceDrawable) { + lightSources.add(lightSource.background as LightSourceDrawable) + } else if (lightSource.foreground is LightSourceDrawable) { + lightSources.add(lightSource.foreground as LightSourceDrawable) } } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt new file mode 100644 index 000000000000..cee71013413d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.Resources +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Outline +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.RadialGradient +import android.graphics.Rect +import android.graphics.Shader +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.MathUtils.lerp +import androidx.annotation.Keep +import com.android.internal.graphics.ColorUtils +import com.android.systemui.Interpolators +import com.android.systemui.R +import org.xmlpull.v1.XmlPullParser + +private const val RIPPLE_ANIM_DURATION = 800L +private const val RIPPLE_DOWN_PROGRESS = 0.05f +private const val RIPPLE_CANCEL_DURATION = 200L +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 LightSourceDrawable : Drawable() { + + private var pressed = false + private var themeAttrs: IntArray? = null + private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f) + private var paint = Paint() + + var highlightColor = Color.WHITE + set(value) { + if (field == value) { + return + } + field = value + invalidateSelf() + } + + /** + * Draw a small highlight under the finger before expanding (or cancelling) it. + */ + private var active: Boolean = false + set(value) { + if (value == field) { + return + } + field = value + + if (value) { + rippleAnimation?.cancel() + rippleData.alpha = 1f + rippleData.progress = RIPPLE_DOWN_PROGRESS + } else { + rippleAnimation?.cancel() + rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { + duration = RIPPLE_CANCEL_DURATION + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.alpha = it.animatedValue as Float + invalidateSelf() + } + addListener(object : AnimatorListenerAdapter() { + var cancelled = false + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + } + + override fun onAnimationEnd(animation: Animator?) { + if (cancelled) { + return + } + rippleData.progress = 0f + rippleData.alpha = 0f + rippleAnimation = null + invalidateSelf() + } + }) + start() + } + } + invalidateSelf() + } + + private var rippleAnimation: Animator? = null + + /** + * Draw background and gradient. + */ + override fun draw(canvas: Canvas) { + val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) + val centerColor = + ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt()) + paint.shader = RadialGradient(rippleData.x, rippleData.y, radius, + intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP) + canvas.drawCircle(rippleData.x, rippleData.y, radius, paint) + } + + override fun getOutline(outline: Outline) { + // No bounds, parent will clip it + } + + 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) + themeAttrs = a.extractThemeAttrs() + updateStateFromTypedArray(a) + a.recycle() + } + + private fun updateStateFromTypedArray(a: TypedArray) { + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) { + rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) { + rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { + rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / + 100f + } + } + + override fun canApplyTheme(): Boolean { + return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme() + } + + override fun applyTheme(t: Resources.Theme) { + super.applyTheme(t) + themeAttrs?.let { + val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable) + updateStateFromTypedArray(a) + 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") + } + + /** + * Draws an animated ripple that expands fading away. + */ + private fun illuminate() { + rippleData.alpha = 1f + 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(rippleData.progress, 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() + } + } + + override fun setHotspot(x: Float, y: Float) { + rippleData.x = x + rippleData.y = y + if (active) { + invalidateSelf() + } + } + + override fun isStateful(): Boolean { + return true + } + + override fun hasFocusStateSpecified(): Boolean { + return true + } + + override fun isProjected(): Boolean { + return true + } + + override fun getDirtyBounds(): Rect { + val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) + val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(), + (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt()) + bounds.union(super.getDirtyBounds()) + return bounds + } + + override fun onStateChange(stateSet: IntArray?): Boolean { + val changed = super.onStateChange(stateSet) + if (stateSet == null) { + return changed + } + + val wasPressed = pressed + var enabled = false + pressed = false + var focused = false + var hovered = false + + for (state in stateSet) { + when (state) { + com.android.internal.R.attr.state_enabled -> { + enabled = true + } + com.android.internal.R.attr.state_focused -> { + focused = true + } + com.android.internal.R.attr.state_pressed -> { + pressed = true + } + com.android.internal.R.attr.state_hovered -> { + hovered = true + } + } + } + + active = enabled && (pressed || focused || hovered) + if (wasPressed && !pressed) { + illuminate() + } + + return changed + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 8e1e1b27cadf..5595201a670f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -304,7 +304,7 @@ public class MediaControlPanel { TextView deviceName = mViewHolder.getSeamlessText(); // Update the outline color - RippleDrawable bkgDrawable = (RippleDrawable) mViewHolder.getSeamless().getBackground(); + RippleDrawable bkgDrawable = (RippleDrawable) mViewHolder.getSeamless().getForeground(); GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0); rect.setStroke(2, deviceName.getCurrentTextColor()); rect.setColor(Color.TRANSPARENT); diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 60c576bd6c34..610e00ddd7f1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -41,7 +41,7 @@ class PlayerViewHolder private constructor(itemView: View) { val artistText = itemView.requireViewById<TextView>(R.id.header_artist) // Output switcher - val seamless = itemView.findViewById<ViewGroup>(R.id.media_seamless) + val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image) val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text) @@ -59,12 +59,12 @@ class PlayerViewHolder private constructor(itemView: View) { init { (player.background as IlluminationDrawable).let { - it.setupTouch(seamless, player) - it.setupTouch(action0, player) - it.setupTouch(action1, player) - it.setupTouch(action2, player) - it.setupTouch(action3, player) - it.setupTouch(action4, player) + it.registerLightSource(seamless) + it.registerLightSource(action0) + it.registerLightSource(action1) + it.registerLightSource(action2) + it.registerLightSource(action3) + it.registerLightSource(action4) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index b71a62c9d1a9..9d2b6f4deb14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -122,7 +122,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(holder.artistText).thenReturn(artistText) seamless = FrameLayout(context) val seamlessBackground = mock(RippleDrawable::class.java) - seamless.setBackground(seamlessBackground) + seamless.foreground = seamlessBackground whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) whenever(holder.seamless).thenReturn(seamless) seamlessIcon = ImageView(context) |