[Media TTT] Add the ripple behind the receiver chip.

This mostly just creates a generic ripple/ package with the relevant
classes and uses it for both the charging ripple and ttt ripple.

The full specs of the ripple (size, duration, interpolation, etc. etc.)
aren't finished yet -- I'll polish these with UX next week.

Bug: 203800342
Test: manual (see video attached to bug)
Test: atest SystemUITests
Change-Id: Icdae0cc189121b90ad88e507135876ce4b1979fa
This commit is contained in:
Caitlin Cassidy
2022-06-21 21:39:53 +00:00
parent 7e0149d26f
commit 8ce9566d3f
14 changed files with 125 additions and 32 deletions

View File

@@ -21,6 +21,12 @@
android:layout_height="wrap_content"
>
<com.android.systemui.media.taptotransfer.receiver.ReceiverChipRippleView
android:id="@+id/ripple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<com.android.internal.widget.CachingIconView
android:id="@+id/app_icon"
android:layout_width="@dimen/media_ttt_icon_size_receiver"

View File

@@ -22,7 +22,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.systemui.statusbar.charging.ChargingRippleView
<com.android.systemui.ripple.RippleView
android:id="@+id/wireless_charging_ripple"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@@ -29,8 +29,7 @@ import android.view.View
import android.view.animation.PathInterpolator
import com.android.internal.graphics.ColorUtils
import com.android.systemui.animation.Interpolators
import com.android.systemui.statusbar.charging.DwellRippleShader
import com.android.systemui.statusbar.charging.RippleShader
import com.android.systemui.ripple.RippleShader
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
@@ -298,7 +297,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
unlockedRippleInProgress = true
rippleShader.shouldFadeOutRipple = true
rippleShader.rippleFill = false
drawRipple = true
visibility = VISIBLE
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.charging
package com.android.systemui.biometrics
import android.graphics.PointF
import android.graphics.RuntimeShader

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.charging
package com.android.systemui.charging
import android.content.Context
import android.content.res.Configuration
@@ -32,6 +32,7 @@ import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.ripple.RippleView
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
@@ -84,7 +85,7 @@ class WiredChargingRippleController @Inject constructor(
private var debounceLevel = 0
@VisibleForTesting
var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
var rippleView: RippleView = RippleView(context, attrs = null)
init {
pluggedIn = batteryController.isPluggedIn

View File

@@ -34,7 +34,7 @@ import android.widget.TextView;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.charging.ChargingRippleView;
import com.android.systemui.ripple.RippleView;
import java.text.NumberFormat;
@@ -46,7 +46,7 @@ public class WirelessChargingLayout extends FrameLayout {
private static final long RIPPLE_ANIMATION_DURATION = 1500;
private static final int SCRIM_COLOR = 0x4C000000;
private static final int SCRIM_FADE_DURATION = 300;
private ChargingRippleView mRippleView;
private RippleView mRippleView;
public WirelessChargingLayout(Context context) {
super(context);

View File

@@ -54,7 +54,7 @@ import com.android.systemui.util.view.ViewUtil
abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>(
internal val context: Context,
internal val logger: MediaTttLogger,
private val windowManager: WindowManager,
internal val windowManager: WindowManager,
private val viewUtil: ViewUtil,
@Main private val mainExecutor: DelayableExecutor,
private val accessibilityManager: AccessibilityManager,

View File

@@ -16,8 +16,10 @@
package com.android.systemui.media.taptotransfer.receiver
import android.annotation.SuppressLint
import android.app.StatusBarManager
import android.content.Context
import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
@@ -29,6 +31,8 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import androidx.core.graphics.ColorUtils
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
@@ -72,9 +76,15 @@ class MediaTttChipControllerReceiver @Inject constructor(
powerManager,
R.layout.media_ttt_chip_receiver
) {
@SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
override val windowLayoutParams = commonWindowLayoutParams.apply {
height = getWindowHeight()
gravity = Gravity.BOTTOM.or(Gravity.CENTER_HORIZONTAL)
// Params below are needed for the ripple to work correctly
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
fitInsetsTypes = 0 // Ignore insets from all system bars
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
}
private val commandQueueCallbacks = object : CommandQueue.Callbacks {
@@ -149,7 +159,7 @@ class MediaTttChipControllerReceiver @Inject constructor(
.alpha(1f)
.setDuration(5.frames)
.start()
startRipple(chipView.requireViewById(R.id.ripple))
}
override fun getIconSize(isAppIcon: Boolean): Int? =
@@ -161,16 +171,44 @@ class MediaTttChipControllerReceiver @Inject constructor(
}
)
private fun getWindowHeight(): Int {
return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) +
// Make the window large enough to accommodate the animation amount
getTranslationAmount()
}
/** Returns the amount that the chip will be translated by in its intro animation. */
private fun getTranslationAmount(): Int {
return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
}
private fun startRipple(rippleView: ReceiverChipRippleView) {
if (rippleView.rippleInProgress) {
// Skip if ripple is still playing
return
}
rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(view: View?) {}
override fun onViewAttachedToWindow(view: View?) {
if (view == null) {
return
}
val attachedRippleView = view as ReceiverChipRippleView
layoutRipple(attachedRippleView)
attachedRippleView.startRipple()
attachedRippleView.removeOnAttachStateChangeListener(this)
}
})
}
private fun layoutRipple(rippleView: ReceiverChipRippleView) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height()
val width = windowBounds.width()
rippleView.radius = height / 5f
// Center the ripple on the bottom of the screen in the middle.
rippleView.origin = PointF(/* x= */ width / 2f, /* y= */ height.toFloat())
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
val colorWithAlpha = ColorUtils.setAlphaComponent(color, 70)
rippleView.setColor(colorWithAlpha)
}
}
data class ChipReceiverInfo(

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 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.taptotransfer.receiver
import android.content.Context
import android.util.AttributeSet
import com.android.systemui.ripple.RippleView
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
*/
class ReceiverChipRippleView(
context: Context?, attrs: AttributeSet?
) : RippleView(context, attrs) {
init {
setRippleFill(true)
duration = 3000L
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.charging
package com.android.systemui.ripple
import android.graphics.PointF
import android.graphics.RuntimeShader
@@ -148,7 +148,7 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
val fadeOutNoise = subProgress(0.4f, 1f, value)
var fadeOutRipple = 0f
var fadeCircle = 0f
if (shouldFadeOutRipple) {
if (!rippleFill) {
fadeCircle = subProgress(0f, 0.2f, value)
fadeOutRipple = subProgress(0.3f, 1f, value)
}
@@ -202,5 +202,9 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
setFloatUniform("in_pixelDensity", value)
}
var shouldFadeOutRipple: Boolean = true
/**
* True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
* False for a ring effect.
*/
var rippleFill: Boolean = false
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.charging
package com.android.systemui.ripple
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@@ -30,9 +30,10 @@ import android.view.View
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
/**
* Expanding ripple effect that shows when charging begins.
* A generic expanding ripple effect. To trigger the ripple expansion, set [radius] and [origin],
* then call [startRipple].
*/
class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val rippleShader = RippleShader()
private val defaultColor: Int = 0xffffffff.toInt()
private val ripplePaint = Paint()
@@ -74,9 +75,9 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
}
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = duration
animator.addUpdateListener { animator ->
val now = animator.currentPlayTime
val progress = animator.animatedValue as Float
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
rippleShader.progress = progress
rippleShader.distortionStrength = 1 - progress
rippleShader.time = now.toFloat()
@@ -92,10 +93,20 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
rippleInProgress = true
}
/** Set the color to be used for the ripple. */
fun setColor(color: Int) {
rippleShader.color = color
}
/**
* Set whether the ripple should remain filled as the ripple expands.
*
* See [RippleShader.rippleFill].
*/
fun setRippleFill(rippleFill: Boolean) {
rippleShader.rippleFill = rippleFill
}
override fun onDraw(canvas: Canvas?) {
if (canvas == null || !canvas.isHardwareAccelerated) {
// Drawing with the ripple shader requires hardware acceleration, so skip
@@ -107,6 +118,6 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
// animation implementation in the ripple shader.
val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
(1 - rippleShader.progress)) * radius * 2
canvas?.drawCircle(origin.x, origin.y, maskRadius, ripplePaint)
canvas.drawCircle(origin.x, origin.y, maskRadius, ripplePaint)
}
}

View File

@@ -139,6 +139,7 @@ import com.android.systemui.assist.AssistManager;
import com.android.systemui.biometrics.AuthRippleController;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.camera.CameraIntents;
import com.android.systemui.charging.WiredChargingRippleController;
import com.android.systemui.charging.WirelessChargingAnimation;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -197,7 +198,6 @@ import com.android.systemui.statusbar.PowerButtonReveal;
import com.android.systemui.statusbar.PulseExpansionHandler;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.charging.WiredChargingRippleController;
import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.core.StatusBarInitializer;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.charging
package com.android.systemui.charging
import android.testing.AndroidTestingRunner
import android.view.View
@@ -24,6 +24,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.ripple.RippleView
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -50,7 +51,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() {
@Mock private lateinit var batteryController: BatteryController
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var configurationController: ConfigurationController
@Mock private lateinit var rippleView: ChargingRippleView
@Mock private lateinit var rippleView: RippleView
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var uiEventLogger: UiEventLogger
private val systemClock = FakeSystemClock()

View File

@@ -84,6 +84,7 @@ import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenu
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.charging.WiredChargingRippleController;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -117,7 +118,6 @@ import com.android.systemui.statusbar.OperatorNameViewController;
import com.android.systemui.statusbar.PulseExpansionHandler;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.charging.WiredChargingRippleController;
import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;