Merge changes Ib7409c94,Ic199a555 into tm-qpr-dev
* changes: Add touch ripple in the media control panel, with flag protected. Add MultiRippleView and its controller to support UMO touch ripple effects.
This commit is contained in:
@@ -44,6 +44,15 @@
|
|||||||
android:background="@drawable/qs_media_outline_album_bg"
|
android:background="@drawable/qs_media_outline_album_bg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<com.android.systemui.ripple.MultiRippleView
|
||||||
|
android:id="@+id/touch_ripple_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/qs_media_session_height_expanded"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/album_art"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/album_art"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/album_art"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/album_art" />
|
||||||
|
|
||||||
<!-- Guideline for output switcher -->
|
<!-- Guideline for output switcher -->
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/center_vertical_guideline"
|
android:id="@+id/center_vertical_guideline"
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<!-- Touch ripple must have the same constraint as the album art. -->
|
||||||
|
<Constraint
|
||||||
|
android:id="@+id/touch_ripple_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/qs_media_session_height_collapsed"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/album_art"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/album_art"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/album_art"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
|
||||||
|
|
||||||
<Constraint
|
<Constraint
|
||||||
android:id="@+id/header_title"
|
android:id="@+id/header_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -27,6 +27,16 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<!-- Touch ripple must have the same constraint as the album art. -->
|
||||||
|
<Constraint
|
||||||
|
android:id="@+id/touch_ripple_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/qs_media_session_height_expanded"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/album_art"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/album_art"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/album_art"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
|
||||||
|
|
||||||
<Constraint
|
<Constraint
|
||||||
android:id="@+id/header_title"
|
android:id="@+id/header_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class WiredChargingRippleController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startRipple() {
|
fun startRipple() {
|
||||||
if (rippleView.rippleInProgress || rippleView.parent != null) {
|
if (rippleView.rippleInProgress() || rippleView.parent != null) {
|
||||||
// Skip if ripple is still playing, or not playing but already added the parent
|
// Skip if ripple is still playing, or not playing but already added the parent
|
||||||
// (which might happen just before the animation starts or right after
|
// (which might happen just before the animation starts or right after
|
||||||
// the animation ends.)
|
// the animation ends.)
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ import android.widget.TextView;
|
|||||||
import com.android.settingslib.Utils;
|
import com.android.settingslib.Utils;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.animation.Interpolators;
|
import com.android.systemui.animation.Interpolators;
|
||||||
|
import com.android.systemui.ripple.RippleAnimationConfig;
|
||||||
import com.android.systemui.ripple.RippleShader.RippleShape;
|
import com.android.systemui.ripple.RippleShader.RippleShape;
|
||||||
import com.android.systemui.ripple.RippleView;
|
import com.android.systemui.ripple.RippleView;
|
||||||
import com.android.systemui.ripple.RippleViewKt;
|
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ final class WirelessChargingLayout extends FrameLayout {
|
|||||||
mRippleView.setColor(color, 28);
|
mRippleView.setColor(color, 28);
|
||||||
} else {
|
} else {
|
||||||
mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
|
mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
|
||||||
mRippleView.setColor(color, RippleViewKt.RIPPLE_DEFAULT_ALPHA);
|
mRippleView.setColor(color, RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
|
OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ object Flags {
|
|||||||
@JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906)
|
@JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906)
|
||||||
|
|
||||||
// TODO(b/254513168): Tracking Bug
|
// TODO(b/254513168): Tracking Bug
|
||||||
val UMO_SURFACE_RIPPLE = UnreleasedFlag(907)
|
@JvmField val UMO_SURFACE_RIPPLE = UnreleasedFlag(907)
|
||||||
|
|
||||||
// 1000 - dock
|
// 1000 - dock
|
||||||
val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000)
|
val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.widget.TextView
|
|||||||
import androidx.constraintlayout.widget.Barrier
|
import androidx.constraintlayout.widget.Barrier
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.media.controls.models.GutsViewHolder
|
import com.android.systemui.media.controls.models.GutsViewHolder
|
||||||
|
import com.android.systemui.ripple.MultiRippleView
|
||||||
import com.android.systemui.util.animation.TransitionLayout
|
import com.android.systemui.util.animation.TransitionLayout
|
||||||
|
|
||||||
private const val TAG = "MediaViewHolder"
|
private const val TAG = "MediaViewHolder"
|
||||||
@@ -36,6 +37,7 @@ class MediaViewHolder constructor(itemView: View) {
|
|||||||
|
|
||||||
// Player information
|
// Player information
|
||||||
val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
|
val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
|
||||||
|
val multiRippleView = itemView.requireViewById<MultiRippleView>(R.id.touch_ripple_view)
|
||||||
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
|
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
|
||||||
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
|
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
|
||||||
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
|
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.android.internal.annotations.VisibleForTesting
|
|||||||
import com.android.settingslib.Utils
|
import com.android.settingslib.Utils
|
||||||
import com.android.systemui.media.controls.models.player.MediaViewHolder
|
import com.android.systemui.media.controls.models.player.MediaViewHolder
|
||||||
import com.android.systemui.monet.ColorScheme
|
import com.android.systemui.monet.ColorScheme
|
||||||
|
import com.android.systemui.ripple.MultiRippleController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
|
* A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
|
||||||
@@ -100,12 +101,14 @@ class ColorSchemeTransition
|
|||||||
internal constructor(
|
internal constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val mediaViewHolder: MediaViewHolder,
|
private val mediaViewHolder: MediaViewHolder,
|
||||||
|
private val multiRippleController: MultiRippleController,
|
||||||
animatingColorTransitionFactory: AnimatingColorTransitionFactory
|
animatingColorTransitionFactory: AnimatingColorTransitionFactory
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
mediaViewHolder: MediaViewHolder
|
mediaViewHolder: MediaViewHolder,
|
||||||
) : this(context, mediaViewHolder, ::AnimatingColorTransition)
|
multiRippleController: MultiRippleController,
|
||||||
|
) : this(context, mediaViewHolder, multiRippleController, ::AnimatingColorTransition)
|
||||||
|
|
||||||
val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
|
val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
|
||||||
val surfaceColor =
|
val surfaceColor =
|
||||||
@@ -125,6 +128,7 @@ internal constructor(
|
|||||||
val accentColorList = ColorStateList.valueOf(accentPrimary)
|
val accentColorList = ColorStateList.valueOf(accentPrimary)
|
||||||
mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
|
mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
|
||||||
mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
|
mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
|
||||||
|
multiRippleController.updateColor(accentPrimary)
|
||||||
}
|
}
|
||||||
|
|
||||||
val accentSecondary =
|
val accentSecondary =
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ import com.android.systemui.bluetooth.BroadcastDialogController;
|
|||||||
import com.android.systemui.broadcast.BroadcastSender;
|
import com.android.systemui.broadcast.BroadcastSender;
|
||||||
import com.android.systemui.dagger.qualifiers.Background;
|
import com.android.systemui.dagger.qualifiers.Background;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
|
import com.android.systemui.flags.FeatureFlags;
|
||||||
|
import com.android.systemui.flags.Flags;
|
||||||
import com.android.systemui.media.controls.models.GutsViewHolder;
|
import com.android.systemui.media.controls.models.GutsViewHolder;
|
||||||
import com.android.systemui.media.controls.models.player.MediaAction;
|
import com.android.systemui.media.controls.models.player.MediaAction;
|
||||||
import com.android.systemui.media.controls.models.player.MediaButton;
|
import com.android.systemui.media.controls.models.player.MediaButton;
|
||||||
@@ -95,6 +97,10 @@ import com.android.systemui.monet.ColorScheme;
|
|||||||
import com.android.systemui.monet.Style;
|
import com.android.systemui.monet.Style;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.plugins.FalsingManager;
|
import com.android.systemui.plugins.FalsingManager;
|
||||||
|
import com.android.systemui.ripple.MultiRippleController;
|
||||||
|
import com.android.systemui.ripple.RippleAnimation;
|
||||||
|
import com.android.systemui.ripple.RippleAnimationConfig;
|
||||||
|
import com.android.systemui.ripple.RippleShader;
|
||||||
import com.android.systemui.shared.system.SysUiStatsLog;
|
import com.android.systemui.shared.system.SysUiStatsLog;
|
||||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
|
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
|
||||||
import com.android.systemui.statusbar.policy.KeyguardStateController;
|
import com.android.systemui.statusbar.policy.KeyguardStateController;
|
||||||
@@ -209,6 +215,8 @@ public class MediaControlPanel {
|
|||||||
private boolean mIsCurrentBroadcastedApp = false;
|
private boolean mIsCurrentBroadcastedApp = false;
|
||||||
private boolean mShowBroadcastDialogButton = false;
|
private boolean mShowBroadcastDialogButton = false;
|
||||||
private String mSwitchBroadcastApp;
|
private String mSwitchBroadcastApp;
|
||||||
|
private MultiRippleController mMultiRippleController;
|
||||||
|
private FeatureFlags mFeatureFlags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a new control panel
|
* Initialize a new control panel
|
||||||
@@ -236,7 +244,9 @@ public class MediaControlPanel {
|
|||||||
KeyguardStateController keyguardStateController,
|
KeyguardStateController keyguardStateController,
|
||||||
ActivityIntentHelper activityIntentHelper,
|
ActivityIntentHelper activityIntentHelper,
|
||||||
NotificationLockscreenUserManager lockscreenUserManager,
|
NotificationLockscreenUserManager lockscreenUserManager,
|
||||||
BroadcastDialogController broadcastDialogController) {
|
BroadcastDialogController broadcastDialogController,
|
||||||
|
FeatureFlags featureFlags
|
||||||
|
) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mBackgroundExecutor = backgroundExecutor;
|
mBackgroundExecutor = backgroundExecutor;
|
||||||
mMainExecutor = mainExecutor;
|
mMainExecutor = mainExecutor;
|
||||||
@@ -262,6 +272,8 @@ public class MediaControlPanel {
|
|||||||
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
|
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mFeatureFlags = featureFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -381,7 +393,9 @@ public class MediaControlPanel {
|
|||||||
AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
|
AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
|
||||||
Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
|
Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
|
||||||
|
|
||||||
mColorSchemeTransition = new ColorSchemeTransition(mContext, mMediaViewHolder);
|
mMultiRippleController = new MultiRippleController(vh.getMultiRippleView());
|
||||||
|
mColorSchemeTransition = new ColorSchemeTransition(
|
||||||
|
mContext, mMediaViewHolder, mMultiRippleController);
|
||||||
mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
|
mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +996,9 @@ public class MediaControlPanel {
|
|||||||
mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
|
mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
|
||||||
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
|
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
|
||||||
action.run();
|
action.run();
|
||||||
|
if (mFeatureFlags.isEnabled(Flags.UMO_SURFACE_RIPPLE)) {
|
||||||
|
mMultiRippleController.play(createTouchRippleAnimation(button));
|
||||||
|
}
|
||||||
|
|
||||||
if (icon instanceof Animatable) {
|
if (icon instanceof Animatable) {
|
||||||
((Animatable) icon).start();
|
((Animatable) icon).start();
|
||||||
@@ -997,6 +1014,26 @@ public class MediaControlPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RippleAnimation createTouchRippleAnimation(ImageButton button) {
|
||||||
|
float maxSize = mMediaViewHolder.getMultiRippleView().getWidth() * 2;
|
||||||
|
return new RippleAnimation(
|
||||||
|
new RippleAnimationConfig(
|
||||||
|
RippleShader.RippleShape.CIRCLE,
|
||||||
|
/* duration= */ 1500L,
|
||||||
|
/* centerX= */ button.getX() + button.getWidth() * 0.5f,
|
||||||
|
/* centerY= */ button.getY() + button.getHeight() * 0.5f,
|
||||||
|
/* maxWidth= */ maxSize,
|
||||||
|
/* maxHeight= */ maxSize,
|
||||||
|
/* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
|
||||||
|
mColorSchemeTransition.getAccentPrimary().getTargetColor(),
|
||||||
|
/* opacity= */ 100,
|
||||||
|
/* shouldFillRipple= */ false,
|
||||||
|
/* sparkleStrength= */ 0f,
|
||||||
|
/* shouldDistort= */ false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void clearButton(final ImageButton button) {
|
private void clearButton(final ImageButton button) {
|
||||||
button.setImageDrawable(null);
|
button.setImageDrawable(null);
|
||||||
button.setContentDescription(null);
|
button.setContentDescription(null);
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class MediaTttChipControllerReceiver @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startRipple(rippleView: ReceiverChipRippleView) {
|
private fun startRipple(rippleView: ReceiverChipRippleView) {
|
||||||
if (rippleView.rippleInProgress) {
|
if (rippleView.rippleInProgress()) {
|
||||||
// Skip if ripple is still playing
|
// Skip if ripple is still playing
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.ripple
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
|
||||||
|
/** Controller that handles playing [RippleAnimation]. */
|
||||||
|
class MultiRippleController(private val multipleRippleView: MultiRippleView) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Max number of ripple animations at a time. */
|
||||||
|
@VisibleForTesting const val MAX_RIPPLE_NUMBER = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates all the ripple colors during the animation. */
|
||||||
|
fun updateColor(color: Int) {
|
||||||
|
multipleRippleView.ripples.forEach { anim -> anim.updateColor(color) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun play(rippleAnimation: RippleAnimation) {
|
||||||
|
if (multipleRippleView.ripples.size >= MAX_RIPPLE_NUMBER) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
multipleRippleView.ripples.add(rippleAnimation)
|
||||||
|
|
||||||
|
// Remove ripple once the animation is done
|
||||||
|
rippleAnimation.play { multipleRippleView.ripples.remove(rippleAnimation) }
|
||||||
|
|
||||||
|
// Trigger drawing
|
||||||
|
multipleRippleView.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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.ripple
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view that allows multiple ripples to play.
|
||||||
|
*
|
||||||
|
* Use [MultiRippleController] to play ripple animations.
|
||||||
|
*/
|
||||||
|
class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
|
||||||
|
|
||||||
|
internal val ripples = ArrayList<RippleAnimation>()
|
||||||
|
private val ripplePaint = Paint()
|
||||||
|
private var isWarningLogged = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "MultiRippleView"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas?) {
|
||||||
|
if (canvas == null || !canvas.isHardwareAccelerated) {
|
||||||
|
// Drawing with the ripple shader requires hardware acceleration, so skip
|
||||||
|
// if it's unsupported.
|
||||||
|
if (!isWarningLogged) {
|
||||||
|
// Only log once to not spam.
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Can't draw ripple shader. $canvas does not support hardware acceleration."
|
||||||
|
)
|
||||||
|
isWarningLogged = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldInvalidate = false
|
||||||
|
|
||||||
|
ripples.forEach { anim ->
|
||||||
|
ripplePaint.shader = anim.rippleShader
|
||||||
|
canvas.drawPaint(ripplePaint)
|
||||||
|
|
||||||
|
shouldInvalidate = shouldInvalidate || anim.isPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldInvalidate) invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.ripple
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
|
||||||
|
/** A single ripple animation. */
|
||||||
|
class RippleAnimation(private val config: RippleAnimationConfig) {
|
||||||
|
internal val rippleShader: RippleShader = RippleShader(config.rippleShape)
|
||||||
|
private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
|
||||||
|
|
||||||
|
init {
|
||||||
|
applyConfigToShader()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the ripple color during the animation. */
|
||||||
|
fun updateColor(color: Int) {
|
||||||
|
config.apply { config.color = color }
|
||||||
|
applyConfigToShader()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun play(onAnimationEnd: Runnable? = null) {
|
||||||
|
if (isPlaying()) {
|
||||||
|
return // Ignore if ripple effect is already playing
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.duration = config.duration
|
||||||
|
animator.addUpdateListener { updateListener ->
|
||||||
|
val now = updateListener.currentPlayTime
|
||||||
|
val progress = updateListener.animatedValue as Float
|
||||||
|
rippleShader.progress = progress
|
||||||
|
rippleShader.distortionStrength = if (config.shouldDistort) 1 - progress else 0f
|
||||||
|
rippleShader.time = now.toFloat()
|
||||||
|
}
|
||||||
|
animator.addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
onAnimationEnd?.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indicates whether the animation is playing. */
|
||||||
|
fun isPlaying(): Boolean = animator.isRunning
|
||||||
|
|
||||||
|
private fun applyConfigToShader() {
|
||||||
|
rippleShader.setCenter(config.centerX, config.centerY)
|
||||||
|
rippleShader.setMaxSize(config.maxWidth, config.maxHeight)
|
||||||
|
rippleShader.rippleFill = config.shouldFillRipple
|
||||||
|
rippleShader.pixelDensity = config.pixelDensity
|
||||||
|
rippleShader.color = ColorUtils.setAlphaComponent(config.color, config.opacity)
|
||||||
|
rippleShader.sparkleStrength = config.sparkleStrength
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.android.systemui.ripple
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A struct that holds the ripple animation configurations.
|
||||||
|
*
|
||||||
|
* <p>This configuration is designed to play a SINGLE animation. Do not reuse or modify the
|
||||||
|
* configuration parameters to play different animations, unless the value has to change within the
|
||||||
|
* single animation (e.g. Change color or opacity during the animation). Note that this data class
|
||||||
|
* is pulled out to make the [RippleAnimation] constructor succinct.
|
||||||
|
*/
|
||||||
|
data class RippleAnimationConfig(
|
||||||
|
val rippleShape: RippleShader.RippleShape = RippleShader.RippleShape.CIRCLE,
|
||||||
|
val duration: Long = 0L,
|
||||||
|
val centerX: Float = 0f,
|
||||||
|
val centerY: Float = 0f,
|
||||||
|
val maxWidth: Float = 0f,
|
||||||
|
val maxHeight: Float = 0f,
|
||||||
|
val pixelDensity: Float = 1f,
|
||||||
|
var color: Int = Color.WHITE,
|
||||||
|
val opacity: Int = RIPPLE_DEFAULT_ALPHA,
|
||||||
|
val shouldFillRipple: Boolean = false,
|
||||||
|
val sparkleStrength: Float = RIPPLE_SPARKLE_STRENGTH,
|
||||||
|
val shouldDistort: Boolean = true
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
|
||||||
|
const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt()
|
||||||
|
const val RIPPLE_DEFAULT_ALPHA: Int = 45 // full opacity is 255.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,10 +28,6 @@ import android.view.View
|
|||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import com.android.systemui.ripple.RippleShader.RippleShape
|
import com.android.systemui.ripple.RippleShader.RippleShape
|
||||||
|
|
||||||
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
|
|
||||||
private const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt()
|
|
||||||
const val RIPPLE_DEFAULT_ALPHA: Int = 45
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic expanding ripple effect.
|
* A generic expanding ripple effect.
|
||||||
*
|
*
|
||||||
@@ -45,8 +41,8 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
private val ripplePaint = Paint()
|
private val ripplePaint = Paint()
|
||||||
|
private val animator = ValueAnimator.ofFloat(0f, 1f)
|
||||||
|
|
||||||
var rippleInProgress: Boolean = false
|
|
||||||
var duration: Long = 1750
|
var duration: Long = 1750
|
||||||
|
|
||||||
private var maxWidth: Float = 0.0f
|
private var maxWidth: Float = 0.0f
|
||||||
@@ -80,9 +76,9 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
|||||||
this.rippleShape = rippleShape
|
this.rippleShape = rippleShape
|
||||||
rippleShader = RippleShader(rippleShape)
|
rippleShader = RippleShader(rippleShape)
|
||||||
|
|
||||||
rippleShader.color = RIPPLE_DEFAULT_COLOR
|
rippleShader.color = RippleAnimationConfig.RIPPLE_DEFAULT_COLOR
|
||||||
rippleShader.progress = 0f
|
rippleShader.progress = 0f
|
||||||
rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
|
rippleShader.sparkleStrength = RippleAnimationConfig.RIPPLE_SPARKLE_STRENGTH
|
||||||
rippleShader.pixelDensity = resources.displayMetrics.density
|
rippleShader.pixelDensity = resources.displayMetrics.density
|
||||||
|
|
||||||
ripplePaint.shader = rippleShader
|
ripplePaint.shader = rippleShader
|
||||||
@@ -90,10 +86,9 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
|||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun startRipple(onAnimationEnd: Runnable? = null) {
|
fun startRipple(onAnimationEnd: Runnable? = null) {
|
||||||
if (rippleInProgress) {
|
if (animator.isRunning) {
|
||||||
return // Ignore if ripple effect is already playing
|
return // Ignore if ripple effect is already playing
|
||||||
}
|
}
|
||||||
val animator = ValueAnimator.ofFloat(0f, 1f)
|
|
||||||
animator.duration = duration
|
animator.duration = duration
|
||||||
animator.addUpdateListener { updateListener ->
|
animator.addUpdateListener { updateListener ->
|
||||||
val now = updateListener.currentPlayTime
|
val now = updateListener.currentPlayTime
|
||||||
@@ -105,19 +100,17 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
|||||||
}
|
}
|
||||||
animator.addListener(object : AnimatorListenerAdapter() {
|
animator.addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
rippleInProgress = false
|
|
||||||
onAnimationEnd?.run()
|
onAnimationEnd?.run()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
animator.start()
|
animator.start()
|
||||||
rippleInProgress = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the color to be used for the ripple.
|
/** Set the color to be used for the ripple.
|
||||||
*
|
*
|
||||||
* The alpha value of the color will be applied to the ripple. The alpha range is [0-100].
|
* The alpha value of the color will be applied to the ripple. The alpha range is [0-100].
|
||||||
*/
|
*/
|
||||||
fun setColor(color: Int, alpha: Int = RIPPLE_DEFAULT_ALPHA) {
|
fun setColor(color: Int, alpha: Int = RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA) {
|
||||||
rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
|
rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +130,9 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
|||||||
rippleShader.sparkleStrength = strength
|
rippleShader.sparkleStrength = strength
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Indicates whether the ripple animation is playing. */
|
||||||
|
fun rippleInProgress(): Boolean = animator.isRunning
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas?) {
|
override fun onDraw(canvas: Canvas?) {
|
||||||
if (canvas == null || !canvas.isHardwareAccelerated) {
|
if (canvas == null || !canvas.isHardwareAccelerated) {
|
||||||
// Drawing with the ripple shader requires hardware acceleration, so skip
|
// Drawing with the ripple shader requires hardware acceleration, so skip
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase
|
|||||||
import com.android.systemui.media.controls.models.GutsViewHolder
|
import com.android.systemui.media.controls.models.GutsViewHolder
|
||||||
import com.android.systemui.media.controls.models.player.MediaViewHolder
|
import com.android.systemui.media.controls.models.player.MediaViewHolder
|
||||||
import com.android.systemui.monet.ColorScheme
|
import com.android.systemui.monet.ColorScheme
|
||||||
|
import com.android.systemui.ripple.MultiRippleController
|
||||||
import junit.framework.Assert.assertEquals
|
import junit.framework.Assert.assertEquals
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -60,6 +61,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() {
|
|||||||
private lateinit var animatingColorTransitionFactory: AnimatingColorTransitionFactory
|
private lateinit var animatingColorTransitionFactory: AnimatingColorTransitionFactory
|
||||||
@Mock private lateinit var mediaViewHolder: MediaViewHolder
|
@Mock private lateinit var mediaViewHolder: MediaViewHolder
|
||||||
@Mock private lateinit var gutsViewHolder: GutsViewHolder
|
@Mock private lateinit var gutsViewHolder: GutsViewHolder
|
||||||
|
@Mock private lateinit var multiRippleController: MultiRippleController
|
||||||
|
|
||||||
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
|
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
|
||||||
|
|
||||||
@@ -70,7 +72,12 @@ class ColorSchemeTransitionTest : SysuiTestCase() {
|
|||||||
whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR)
|
whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR)
|
||||||
|
|
||||||
colorSchemeTransition =
|
colorSchemeTransition =
|
||||||
ColorSchemeTransition(context, mediaViewHolder, animatingColorTransitionFactory)
|
ColorSchemeTransition(
|
||||||
|
context,
|
||||||
|
mediaViewHolder,
|
||||||
|
multiRippleController,
|
||||||
|
animatingColorTransitionFactory
|
||||||
|
)
|
||||||
|
|
||||||
colorTransition =
|
colorTransition =
|
||||||
object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) {
|
object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) {
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ import com.android.systemui.R
|
|||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.bluetooth.BroadcastDialogController
|
import com.android.systemui.bluetooth.BroadcastDialogController
|
||||||
import com.android.systemui.broadcast.BroadcastSender
|
import com.android.systemui.broadcast.BroadcastSender
|
||||||
|
import com.android.systemui.flags.FakeFeatureFlags
|
||||||
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.media.controls.MediaTestUtils
|
import com.android.systemui.media.controls.MediaTestUtils
|
||||||
import com.android.systemui.media.controls.models.GutsViewHolder
|
import com.android.systemui.media.controls.models.GutsViewHolder
|
||||||
import com.android.systemui.media.controls.models.player.MediaAction
|
import com.android.systemui.media.controls.models.player.MediaAction
|
||||||
@@ -76,6 +78,7 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
|
|||||||
import com.android.systemui.media.dialog.MediaOutputDialogFactory
|
import com.android.systemui.media.dialog.MediaOutputDialogFactory
|
||||||
import com.android.systemui.plugins.ActivityStarter
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
import com.android.systemui.plugins.FalsingManager
|
import com.android.systemui.plugins.FalsingManager
|
||||||
|
import com.android.systemui.ripple.MultiRippleView
|
||||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager
|
import com.android.systemui.statusbar.NotificationLockscreenUserManager
|
||||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||||
import com.android.systemui.util.animation.TransitionLayout
|
import com.android.systemui.util.animation.TransitionLayout
|
||||||
@@ -174,6 +177,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
private lateinit var cancelText: TextView
|
private lateinit var cancelText: TextView
|
||||||
private lateinit var dismiss: FrameLayout
|
private lateinit var dismiss: FrameLayout
|
||||||
private lateinit var dismissText: TextView
|
private lateinit var dismissText: TextView
|
||||||
|
private lateinit var multiRippleView: MultiRippleView
|
||||||
|
|
||||||
private lateinit var session: MediaSession
|
private lateinit var session: MediaSession
|
||||||
private lateinit var device: MediaDeviceData
|
private lateinit var device: MediaDeviceData
|
||||||
@@ -205,6 +209,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
private lateinit var recSubtitle2: TextView
|
private lateinit var recSubtitle2: TextView
|
||||||
private lateinit var recSubtitle3: TextView
|
private lateinit var recSubtitle3: TextView
|
||||||
private var shouldShowBroadcastButton: Boolean = false
|
private var shouldShowBroadcastButton: Boolean = false
|
||||||
|
private val fakeFeatureFlag =
|
||||||
|
FakeFeatureFlags().apply { this.set(Flags.UMO_SURFACE_RIPPLE, false) }
|
||||||
|
|
||||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||||
|
|
||||||
@@ -244,7 +250,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
keyguardStateController,
|
keyguardStateController,
|
||||||
activityIntentHelper,
|
activityIntentHelper,
|
||||||
lockscreenUserManager,
|
lockscreenUserManager,
|
||||||
broadcastDialogController
|
broadcastDialogController,
|
||||||
|
fakeFeatureFlag
|
||||||
) {
|
) {
|
||||||
override fun loadAnimator(
|
override fun loadAnimator(
|
||||||
animId: Int,
|
animId: Int,
|
||||||
@@ -374,6 +381,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
multiRippleView = MultiRippleView(context, null)
|
||||||
|
|
||||||
whenever(viewHolder.player).thenReturn(view)
|
whenever(viewHolder.player).thenReturn(view)
|
||||||
whenever(viewHolder.appIcon).thenReturn(appIcon)
|
whenever(viewHolder.appIcon).thenReturn(appIcon)
|
||||||
whenever(viewHolder.albumView).thenReturn(albumView)
|
whenever(viewHolder.albumView).thenReturn(albumView)
|
||||||
@@ -414,6 +423,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
whenever(viewHolder.getAction(R.id.action4)).thenReturn(action4)
|
whenever(viewHolder.getAction(R.id.action4)).thenReturn(action4)
|
||||||
|
|
||||||
whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
|
whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
|
||||||
|
|
||||||
|
whenever(viewHolder.multiRippleView).thenReturn(multiRippleView)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initialize elements for the recommendation view holder */
|
/** Initialize elements for the recommendation view holder */
|
||||||
@@ -1973,6 +1984,50 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
|
assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
|
||||||
|
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
|
||||||
|
val semanticActions =
|
||||||
|
MediaButton(
|
||||||
|
playOrPause =
|
||||||
|
MediaAction(
|
||||||
|
icon = null,
|
||||||
|
action = {},
|
||||||
|
contentDescription = "play",
|
||||||
|
background = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val data = mediaData.copy(semanticActions = semanticActions)
|
||||||
|
player.attachPlayer(viewHolder)
|
||||||
|
player.bindPlayer(data, KEY)
|
||||||
|
|
||||||
|
viewHolder.actionPlayPause.callOnClick()
|
||||||
|
|
||||||
|
assertThat(viewHolder.multiRippleView.ripples.size).isEqualTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onButtonClick_touchRippleFlagDisabled_doesNotPlayTouchRipple() {
|
||||||
|
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, false)
|
||||||
|
val semanticActions =
|
||||||
|
MediaButton(
|
||||||
|
playOrPause =
|
||||||
|
MediaAction(
|
||||||
|
icon = null,
|
||||||
|
action = {},
|
||||||
|
contentDescription = "play",
|
||||||
|
background = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val data = mediaData.copy(semanticActions = semanticActions)
|
||||||
|
player.attachPlayer(viewHolder)
|
||||||
|
player.bindPlayer(data, KEY)
|
||||||
|
|
||||||
|
viewHolder.actionPlayPause.callOnClick()
|
||||||
|
|
||||||
|
assertThat(viewHolder.multiRippleView.ripples.size).isEqualTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
|
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
|
||||||
withArgCaptor {
|
withArgCaptor {
|
||||||
verify(seekBarViewModel).setScrubbingChangeListener(capture())
|
verify(seekBarViewModel).setScrubbingChangeListener(capture())
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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.ripple
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER
|
||||||
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class MultiRippleControllerTest : SysuiTestCase() {
|
||||||
|
private lateinit var multiRippleController: MultiRippleController
|
||||||
|
private lateinit var multiRippleView: MultiRippleView
|
||||||
|
private lateinit var rippleAnimationConfig: RippleAnimationConfig
|
||||||
|
private val fakeSystemClock = FakeSystemClock()
|
||||||
|
|
||||||
|
// FakeExecutor is needed to run animator.
|
||||||
|
private val fakeExecutor = FakeExecutor(fakeSystemClock)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
rippleAnimationConfig = RippleAnimationConfig(duration = 1000L)
|
||||||
|
multiRippleView = MultiRippleView(context, null)
|
||||||
|
multiRippleController = MultiRippleController(multiRippleView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateColor_updatesColor() {
|
||||||
|
val initialColor = Color.WHITE
|
||||||
|
val expectedColor = Color.RED
|
||||||
|
|
||||||
|
fakeExecutor.execute {
|
||||||
|
val rippleAnimation =
|
||||||
|
RippleAnimation(rippleAnimationConfig.apply { this.color = initialColor })
|
||||||
|
|
||||||
|
with(multiRippleController) {
|
||||||
|
play(rippleAnimation)
|
||||||
|
updateColor(expectedColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(rippleAnimationConfig.color).isEqualTo(expectedColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun play_playsRipple() {
|
||||||
|
fakeExecutor.execute {
|
||||||
|
val rippleAnimation = RippleAnimation(rippleAnimationConfig)
|
||||||
|
|
||||||
|
multiRippleController.play(rippleAnimation)
|
||||||
|
|
||||||
|
assertThat(multiRippleView.ripples.size).isEqualTo(1)
|
||||||
|
assertThat(multiRippleView.ripples[0]).isEqualTo(rippleAnimation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun play_doesNotExceedMaxRipple() {
|
||||||
|
fakeExecutor.execute {
|
||||||
|
for (i in 0..MAX_RIPPLE_NUMBER + 10) {
|
||||||
|
multiRippleController.play(RippleAnimation(rippleAnimationConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(multiRippleView.ripples.size).isEqualTo(MAX_RIPPLE_NUMBER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun play_onEnd_removesAnimation() {
|
||||||
|
fakeExecutor.execute {
|
||||||
|
val rippleAnimation = RippleAnimation(rippleAnimationConfig)
|
||||||
|
multiRippleController.play(rippleAnimation)
|
||||||
|
|
||||||
|
assertThat(multiRippleView.ripples.size).isEqualTo(1)
|
||||||
|
assertThat(multiRippleView.ripples[0]).isEqualTo(rippleAnimation)
|
||||||
|
|
||||||
|
fakeSystemClock.advanceTime(rippleAnimationConfig.duration)
|
||||||
|
|
||||||
|
assertThat(multiRippleView.ripples.size).isEqualTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* 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.ripple
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class RippleAnimationTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
private val fakeSystemClock = FakeSystemClock()
|
||||||
|
private val fakeExecutor = FakeExecutor(fakeSystemClock)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun init_shaderHasCorrectConfig() {
|
||||||
|
val config =
|
||||||
|
RippleAnimationConfig(
|
||||||
|
duration = 3000L,
|
||||||
|
pixelDensity = 2f,
|
||||||
|
color = Color.RED,
|
||||||
|
opacity = 30,
|
||||||
|
shouldFillRipple = true,
|
||||||
|
sparkleStrength = 0.3f
|
||||||
|
)
|
||||||
|
val rippleAnimation = RippleAnimation(config)
|
||||||
|
|
||||||
|
with(rippleAnimation.rippleShader) {
|
||||||
|
assertThat(rippleFill).isEqualTo(config.shouldFillRipple)
|
||||||
|
assertThat(pixelDensity).isEqualTo(config.pixelDensity)
|
||||||
|
assertThat(color).isEqualTo(ColorUtils.setAlphaComponent(config.color, config.opacity))
|
||||||
|
assertThat(sparkleStrength).isEqualTo(config.sparkleStrength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateColor_updatesColorCorrectly() {
|
||||||
|
val initialColor = Color.WHITE
|
||||||
|
val expectedColor = Color.RED
|
||||||
|
val config = RippleAnimationConfig(color = initialColor)
|
||||||
|
val rippleAnimation = RippleAnimation(config)
|
||||||
|
|
||||||
|
fakeExecutor.execute {
|
||||||
|
with(rippleAnimation) {
|
||||||
|
play()
|
||||||
|
updateColor(expectedColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(config.color).isEqualTo(expectedColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun play_updatesIsPlaying() {
|
||||||
|
val config = RippleAnimationConfig(duration = 1000L)
|
||||||
|
val rippleAnimation = RippleAnimation(config)
|
||||||
|
|
||||||
|
fakeExecutor.execute {
|
||||||
|
rippleAnimation.play()
|
||||||
|
|
||||||
|
assertThat(rippleAnimation.isPlaying()).isTrue()
|
||||||
|
|
||||||
|
// move time to finish the animation
|
||||||
|
fakeSystemClock.advanceTime(config.duration)
|
||||||
|
|
||||||
|
assertThat(rippleAnimation.isPlaying()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun play_onEnd_triggersOnAnimationEnd() {
|
||||||
|
val config = RippleAnimationConfig(duration = 1000L)
|
||||||
|
val rippleAnimation = RippleAnimation(config)
|
||||||
|
var animationEnd = false
|
||||||
|
|
||||||
|
fakeExecutor.execute {
|
||||||
|
rippleAnimation.play(onAnimationEnd = { animationEnd = true })
|
||||||
|
|
||||||
|
fakeSystemClock.advanceTime(config.duration)
|
||||||
|
|
||||||
|
assertThat(animationEnd).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user