Use DoubleShadowIconDrawable to draw shadow behind archived cloud icon
- Refactor Launcher3 ShadowInfo out of DoubleShadowBubbleTextView
Bug: 350758155
Test: locally verified + screenshot test
Flag: com.android.launcher3.use_new_icon_for_archived_apps
Change-Id: I8e5e5936296fafbfbf3caf80a936ca1038c125e3
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index af91b5a..5e1d8a5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -306,6 +306,7 @@
<dimen name="blur_size_medium_outline">2dp</dimen>
<dimen name="blur_size_click_shadow">4dp</dimen>
<dimen name="click_shadow_high_shift">2dp</dimen>
+ <dimen name="app_title_icon_shadow_inset">1dp</dimen>
<!-- Pending widget -->
<dimen name="pending_widget_min_padding">8dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2eb5034..1eccbff 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -873,7 +873,7 @@
if (drawable == null) {
setText(text);
Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
- + ", will just set text instead. text=" + text);
+ + ", will just set text instead.");
return;
}
drawable.setTint(getCurrentTextColor());
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index bc66a33..ef66ffe 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -19,11 +19,19 @@
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import android.content.Context;
-import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
import android.util.AttributeSet;
-import android.widget.TextView;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
@@ -45,22 +53,65 @@
public DoubleShadowBubbleTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mShadowInfo = new ShadowInfo(context, attrs, defStyle);
- setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, mShadowInfo.ambientShadowColor);
+ mShadowInfo = ShadowInfo.Companion.fromContext(context, attrs, defStyle);
+ setShadowLayer(
+ mShadowInfo.getAmbientShadowBlur(),
+ 0,
+ 0,
+ mShadowInfo.getAmbientShadowColor()
+ );
+ }
+
+ @Override
+ public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) {
+ Drawable drawable = getContext().getDrawable(drawableId);
+ if (drawable == null) {
+ setText(text);
+ Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
+ + ", will just set text instead.");
+ return;
+ }
+ drawable.setTint(getCurrentTextColor());
+ int textSize = Math.round(getTextSize());
+ ImageSpan imageSpan;
+ if (!skipDoubleShadow() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ drawable = getDoubleShadowDrawable(drawable, textSize);
+ }
+ drawable.setBounds(0, 0, textSize, textSize);
+ imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
+ // First space will be replaced with Drawable, second space is for space before text.
+ SpannableString spannable = new SpannableString(" " + text);
+ spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ setText(spannable);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ private DoubleShadowIconDrawable getDoubleShadowDrawable(
+ @NonNull Drawable drawable, int textSize
+ ) {
+ // add some padding via inset to avoid shadow clipping
+ int iconInsetSize = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.app_title_icon_shadow_inset);
+ return new DoubleShadowIconDrawable(
+ mShadowInfo,
+ drawable,
+ textSize,
+ iconInsetSize
+ );
}
@Override
public void onDraw(Canvas canvas) {
// If text is transparent or shadow alpha is 0, don't draw any shadow
- if (mShadowInfo.skipDoubleShadow(this)) {
+ if (skipDoubleShadow()) {
super.onDraw(canvas);
return;
}
int alpha = Color.alpha(getCurrentTextColor());
// We enhance the shadow by drawing the shadow twice
- getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0,
- getTextShadowColor(mShadowInfo.ambientShadowColor, alpha));
+ getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+ getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha));
drawWithoutDot(canvas);
canvas.save();
@@ -69,10 +120,10 @@
getScrollY() + getHeight());
getPaint().setShadowLayer(
- mShadowInfo.keyShadowBlur,
- mShadowInfo.keyShadowOffsetX,
- mShadowInfo.keyShadowOffsetY,
- getTextShadowColor(mShadowInfo.keyShadowColor, alpha));
+ mShadowInfo.getKeyShadowBlur(),
+ mShadowInfo.getKeyShadowOffsetX(),
+ mShadowInfo.getKeyShadowOffsetY(),
+ getTextShadowColor(mShadowInfo.getKeyShadowColor(), alpha));
drawWithoutDot(canvas);
canvas.restore();
@@ -80,55 +131,30 @@
drawRunningAppIndicatorIfNecessary(canvas);
}
- public static class ShadowInfo {
- public final float ambientShadowBlur;
- public final int ambientShadowColor;
-
- public final float keyShadowBlur;
- public final float keyShadowOffsetX;
- public final float keyShadowOffsetY;
- public final int keyShadowColor;
-
- public ShadowInfo(Context c, AttributeSet attrs, int defStyle) {
-
- TypedArray a = c.obtainStyledAttributes(
- attrs, R.styleable.ShadowInfo, defStyle, 0);
-
- ambientShadowBlur = a.getDimensionPixelSize(
- R.styleable.ShadowInfo_ambientShadowBlur, 0);
- ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0);
-
- keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0);
- keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0);
- keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0);
- keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0);
- a.recycle();
- }
-
- public boolean skipDoubleShadow(TextView textView) {
- int textAlpha = Color.alpha(textView.getCurrentTextColor());
- int keyShadowAlpha = Color.alpha(keyShadowColor);
- int ambientShadowAlpha = Color.alpha(ambientShadowColor);
- if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
- textView.getPaint().clearShadowLayer();
- return true;
- } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
- textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0,
- getTextShadowColor(ambientShadowColor, textAlpha));
- return true;
- } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
- textView.getPaint().setShadowLayer(
- keyShadowBlur,
- keyShadowOffsetX,
- keyShadowOffsetY,
- getTextShadowColor(keyShadowColor, textAlpha));
- return true;
- } else {
- return false;
- }
+ private boolean skipDoubleShadow() {
+ int textAlpha = Color.alpha(getCurrentTextColor());
+ int keyShadowAlpha = Color.alpha(mShadowInfo.getKeyShadowColor());
+ int ambientShadowAlpha = Color.alpha(mShadowInfo.getAmbientShadowColor());
+ if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
+ getPaint().clearShadowLayer();
+ return true;
+ } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
+ getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+ getTextShadowColor(mShadowInfo.getAmbientShadowColor(), textAlpha));
+ return true;
+ } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
+ getPaint().setShadowLayer(
+ mShadowInfo.getKeyShadowBlur(),
+ mShadowInfo.getKeyShadowOffsetX(),
+ mShadowInfo.getKeyShadowOffsetY(),
+ getTextShadowColor(mShadowInfo.getKeyShadowColor(), textAlpha));
+ return true;
+ } else {
+ return false;
}
}
+
// Multiplies the alpha of shadowColor by textAlpha.
private static int getTextShadowColor(int shadowColor, int textAlpha) {
return setColorAlphaBound(shadowColor,
diff --git a/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
new file mode 100644
index 0000000..7ac7c94
--- /dev/null
+++ b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 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.launcher3.views
+
+import android.content.res.ColorStateList
+import android.graphics.BlendMode
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.graphics.Shader
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.InsetDrawable
+import android.os.Build.VERSION_CODES
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Launcher wrapper for Drawables to provide a double shadow effect. Currently for use with
+ * [DoubleShadowBubbleTextView] to provide a similar shadow to inline icons.
+ */
+@RequiresApi(VERSION_CODES.S)
+class DoubleShadowIconDrawable(
+ private val shadowInfo: ShadowInfo,
+ iconDrawable: Drawable,
+ private val iconSize: Int,
+ iconInsetSize: Int
+) : Drawable() {
+ private val mIconDrawable: InsetDrawable
+ private val mDoubleShadowNode: RenderNode?
+
+ init {
+ mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize)
+ mIconDrawable.setBounds(0, 0, iconSize, iconSize)
+ mDoubleShadowNode = createShadowRenderNode()
+ }
+
+ @VisibleForTesting
+ fun createShadowRenderNode(): RenderNode {
+ val renderNode = RenderNode("DoubleShadowNode")
+ renderNode.setPosition(0, 0, iconSize, iconSize)
+ // Create render effects
+ val ambientShadow =
+ createShadowRenderEffect(
+ shadowInfo.ambientShadowBlur,
+ 0f,
+ 0f,
+ Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+ )
+ val keyShadow =
+ createShadowRenderEffect(
+ shadowInfo.keyShadowBlur,
+ shadowInfo.keyShadowOffsetX,
+ shadowInfo.keyShadowOffsetY,
+ Color.alpha(shadowInfo.keyShadowColor).toFloat()
+ )
+ val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP)
+ renderNode.setRenderEffect(blend)
+ return renderNode
+ }
+
+ @VisibleForTesting
+ fun createShadowRenderEffect(
+ radius: Float,
+ offsetX: Float,
+ offsetY: Float,
+ alpha: Float
+ ): RenderEffect {
+ return RenderEffect.createColorFilterEffect(
+ PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY),
+ RenderEffect.createOffsetEffect(
+ offsetX,
+ offsetY,
+ RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
+ )
+ )
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (canvas.isHardwareAccelerated && mDoubleShadowNode != null) {
+ if (!mDoubleShadowNode.hasDisplayList()) {
+ // Record render node if its display list is not recorded or discarded
+ // (which happens when it's no longer drawn by anything).
+ val recordingCanvas = mDoubleShadowNode.beginRecording()
+ mIconDrawable.draw(recordingCanvas)
+ mDoubleShadowNode.endRecording()
+ }
+ canvas.drawRenderNode(mDoubleShadowNode)
+ }
+ mIconDrawable.draw(canvas)
+ }
+
+ override fun getIntrinsicHeight() = iconSize
+
+ override fun getIntrinsicWidth() = iconSize
+
+ override fun getOpacity() = PixelFormat.TRANSPARENT
+
+ override fun setAlpha(alpha: Int) {
+ mIconDrawable.alpha = alpha
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ mIconDrawable.colorFilter = colorFilter
+ }
+
+ override fun setTint(color: Int) {
+ mIconDrawable.setTint(color)
+ }
+
+ override fun setTintList(tint: ColorStateList?) {
+ mIconDrawable.setTintList(tint)
+ }
+}
diff --git a/src/com/android/launcher3/views/ShadowInfo.kt b/src/com/android/launcher3/views/ShadowInfo.kt
new file mode 100644
index 0000000..4f626ec
--- /dev/null
+++ b/src/com/android/launcher3/views/ShadowInfo.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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.launcher3.views
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.launcher3.R
+
+/**
+ * Launcher data holder for classes such as [DoubleShadowBubbleTextView] to model shadows for
+ * "double shadow" effect.
+ */
+data class ShadowInfo(
+ val ambientShadowBlur: Float,
+ val ambientShadowColor: Int,
+ val keyShadowBlur: Float,
+ val keyShadowOffsetX: Float,
+ val keyShadowOffsetY: Float,
+ val keyShadowColor: Int
+) {
+
+ companion object {
+ /** Constructs instance of ShadowInfo from Context and given attribute set. */
+ @JvmStatic
+ fun fromContext(context: Context, attrs: AttributeSet?, defStyle: Int): ShadowInfo {
+ val styledAttrs =
+ context.obtainStyledAttributes(attrs, R.styleable.ShadowInfo, defStyle, 0)
+ val shadowInfo =
+ ShadowInfo(
+ ambientShadowBlur =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+ .toFloat(),
+ ambientShadowColor =
+ styledAttrs.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0),
+ keyShadowBlur =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+ .toFloat(),
+ keyShadowOffsetX =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+ .toFloat(),
+ keyShadowOffsetY =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+ .toFloat(),
+ keyShadowColor = styledAttrs.getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+ )
+ styledAttrs.recycle()
+ return shadowInfo
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
new file mode 100644
index 0000000..1cee71c
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 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.launcher3.ui
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.views.DoubleShadowIconDrawable
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DoubleShadowIconDrawableTest {
+
+ @Test
+ fun `DoubleShadowIconDrawable is setup correctly from given ShadowInfo`() {
+ // Given
+ val shadowInfo: ShadowInfo = mock()
+ val originalDrawable: Drawable = mock()
+ val iconSize = 2
+ val iconInsetSize = 1
+ // When
+ val drawableUnderTest =
+ DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize)
+ // Then
+ assertThat(drawableUnderTest.intrinsicHeight).isEqualTo(iconSize)
+ assertThat(drawableUnderTest.intrinsicWidth).isEqualTo(iconSize)
+ }
+
+ @Test
+ fun `createShadowRenderNode creates RenderNode for shadow effects`() {
+ // Given
+ val shadowInfo =
+ ShadowInfo(
+ ambientShadowBlur = 1f,
+ ambientShadowColor = 2,
+ keyShadowBlur = 3f,
+ keyShadowOffsetX = 4f,
+ keyShadowOffsetY = 5f,
+ keyShadowColor = 6
+ )
+ val originalDrawable: Drawable = mock()
+ val iconSize = 2
+ val iconInsetSize = 1
+ // When
+ val shadowDrawableUnderTest =
+ spy(DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize))
+ shadowDrawableUnderTest.createShadowRenderNode()
+ // Then
+ verify(shadowDrawableUnderTest)
+ .createShadowRenderEffect(
+ shadowInfo.ambientShadowBlur,
+ 0f,
+ 0f,
+ Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+ )
+ verify(shadowDrawableUnderTest)
+ .createShadowRenderEffect(
+ shadowInfo.keyShadowBlur,
+ shadowInfo.keyShadowOffsetX,
+ shadowInfo.keyShadowOffsetY,
+ Color.alpha(shadowInfo.keyShadowColor).toFloat()
+ )
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
new file mode 100644
index 0000000..ef4dc1a
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 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.launcher3.ui
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadowInfoTest {
+
+ @Test
+ fun `ShadowInfo is created correctly from context`() {
+ // Given
+ val mockContext: Context = mock()
+ val mockAttrs: AttributeSet = mock()
+ val styledAttrs: TypedArray = mock()
+ val expectedShadowInfo =
+ ShadowInfo(
+ ambientShadowBlur = 1f,
+ ambientShadowColor = 2,
+ keyShadowBlur = 3f,
+ keyShadowOffsetX = 4f,
+ keyShadowOffsetY = 5f,
+ keyShadowColor = 6
+ )
+ doReturn(styledAttrs)
+ .whenever(mockContext)
+ .obtainStyledAttributes(mockAttrs, R.styleable.ShadowInfo, 0, 0)
+ doReturn(1)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+ doReturn(2).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_ambientShadowColor, 0)
+ doReturn(3)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+ doReturn(4)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+ doReturn(5)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+ doReturn(6).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+ // When
+ val actualShadowInfo = ShadowInfo.fromContext(mockContext, mockAttrs, 0)
+ // Then
+ assertThat(actualShadowInfo.ambientShadowBlur).isEqualTo(1)
+ assertThat(actualShadowInfo.ambientShadowColor).isEqualTo(2)
+ assertThat(actualShadowInfo.keyShadowBlur).isEqualTo(3)
+ assertThat(actualShadowInfo.keyShadowOffsetX).isEqualTo(4)
+ assertThat(actualShadowInfo.keyShadowOffsetY).isEqualTo(5)
+ assertThat(actualShadowInfo.keyShadowColor).isEqualTo(6)
+ assertThat(actualShadowInfo).isEqualTo(expectedShadowInfo)
+ }
+}