From 86e8b00ecdc9f9c5212e5a7e0700bd95be4da94b Mon Sep 17 00:00:00 2001 From: pekingme <8545955+pekingme@users.noreply.github.com> Date: Thu, 10 Feb 2022 20:07:58 +0000 Subject: [PATCH] [CollapsingToolbarLayout] Fixed shadow issues when transition between expanded and collapsed states. Includes two main fixes: 1. Applied a workaround for alpha issue of shadow layer in Paint for API 31(+). - Issue: For API 31 and plus, Paint doesn't apply the correct alpha value to the shadow layer. When the shadow color is opaque (alpha=255), the alpha of shadow should use the alpha of the Paint. But it seems the inverse alpha (255-alpha) is applied to the shadow layer. - Solution: Added a workaround to set shadow color with a real-time alpha value, Paint still respects to the shadow color's alpha when it's not opaque. 2. Fixed rounding issue when blending shadow color for API 30(-). - Issue: The rounding of alpha value in removed blendColors() function is casting (round down). This causes the shadow color occasionally becomes non-opaque. Then Paint stops to apply the alpha value to it. - Solution: When blending colors, round values with Math.round() (round to nearest). Note: Fix (1) covers fix (2). But fix (2) is still worth to keep, if issue (1) is fixed in the framework in future and workaround is removed. Resolves: https://github.com/material-components/material-components-android/issues/2545 PiperOrigin-RevId: 427812326 --- .../internal/CollapsingTextHelper.java | 126 ++++++++++++------ 1 file changed, 84 insertions(+), 42 deletions(-) diff --git a/lib/java/com/google/android/material/internal/CollapsingTextHelper.java b/lib/java/com/google/android/material/internal/CollapsingTextHelper.java index cf4a98e1cfb..09978f5eddf 100644 --- a/lib/java/com/google/android/material/internal/CollapsingTextHelper.java +++ b/lib/java/com/google/android/material/internal/CollapsingTextHelper.java @@ -56,6 +56,7 @@ import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import com.google.android.material.animation.AnimationUtils; +import com.google.android.material.color.MaterialColors; import com.google.android.material.internal.StaticLayoutBuilderCompat.StaticLayoutBuilderCompatException; import com.google.android.material.resources.CancelableFontCallback; import com.google.android.material.resources.CancelableFontCallback.ApplyFont; @@ -136,6 +137,10 @@ public final class CollapsingTextHelper { private float scale; private float currentTextSize; + private float currentShadowRadius; + private float currentShadowDx; + private float currentShadowDy; + private int currentShadowColor; private int[] state; @@ -471,11 +476,11 @@ private boolean setCollapsedTypefaceInternal(Typeface typeface) { } if (collapsedTypefaceDefault != typeface) { collapsedTypefaceDefault = typeface; - collapsedTypefaceBold = TypefaceUtils.maybeCopyWithFontWeightAdjustment( - view.getContext().getResources().getConfiguration(), - typeface); - collapsedTypeface = collapsedTypefaceBold == null - ? collapsedTypefaceDefault : collapsedTypefaceBold; + collapsedTypefaceBold = + TypefaceUtils.maybeCopyWithFontWeightAdjustment( + view.getContext().getResources().getConfiguration(), typeface); + collapsedTypeface = + collapsedTypefaceBold == null ? collapsedTypefaceDefault : collapsedTypefaceBold; return true; } return false; @@ -490,11 +495,11 @@ private boolean setExpandedTypefaceInternal(Typeface typeface) { } if (expandedTypefaceDefault != typeface) { expandedTypefaceDefault = typeface; - expandedTypefaceBold = TypefaceUtils.maybeCopyWithFontWeightAdjustment( - view.getContext().getResources().getConfiguration(), - typeface); - expandedTypeface = expandedTypefaceBold == null - ? expandedTypefaceDefault : expandedTypefaceBold; + expandedTypefaceBold = + TypefaceUtils.maybeCopyWithFontWeightAdjustment( + view.getContext().getResources().getConfiguration(), typeface); + expandedTypeface = + expandedTypefaceBold == null ? expandedTypefaceDefault : expandedTypefaceBold; return true; } return false; @@ -511,20 +516,19 @@ public Typeface getExpandedTypeface() { public void maybeUpdateFontWeightAdjustment(@NonNull Configuration configuration) { if (VERSION.SDK_INT >= VERSION_CODES.S) { if (collapsedTypefaceDefault != null) { - collapsedTypefaceBold = TypefaceUtils.maybeCopyWithFontWeightAdjustment( - configuration, - collapsedTypefaceDefault); + collapsedTypefaceBold = + TypefaceUtils.maybeCopyWithFontWeightAdjustment( + configuration, collapsedTypefaceDefault); } if (expandedTypefaceDefault != null) { - expandedTypefaceBold = TypefaceUtils.maybeCopyWithFontWeightAdjustment( - configuration, - expandedTypefaceDefault); + expandedTypefaceBold = + TypefaceUtils.maybeCopyWithFontWeightAdjustment(configuration, expandedTypefaceDefault); } - collapsedTypeface = collapsedTypefaceBold != null - ? collapsedTypefaceBold : collapsedTypefaceDefault; - expandedTypeface = expandedTypefaceBold != null - ? expandedTypefaceBold : expandedTypefaceDefault; - recalculate(/* forceRecalculate= */true); + collapsedTypeface = + collapsedTypefaceBold != null ? collapsedTypefaceBold : collapsedTypefaceDefault; + expandedTypeface = + expandedTypefaceBold != null ? expandedTypefaceBold : expandedTypefaceDefault; + recalculate(/* forceRecalculate= */ true); } } @@ -621,7 +625,7 @@ private void calculateOffsets(final float fraction) { // If the collapsed and expanded text colors are different, blend them based on the // fraction textPaint.setColor( - blendColors( + blendARGB( getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), textBlendFraction)); } else { textPaint.setColor(getCurrentCollapsedTextColor()); @@ -640,12 +644,15 @@ private void calculateOffsets(final float fraction) { } } + // Calculates paint parameters for shadow layer. + currentShadowRadius = lerp(expandedShadowRadius, collapsedShadowRadius, fraction, null); + currentShadowDx = lerp(expandedShadowDx, collapsedShadowDx, fraction, null); + currentShadowDy = lerp(expandedShadowDy, collapsedShadowDy, fraction, null); + currentShadowColor = + blendARGB( + getCurrentColor(expandedShadowColor), getCurrentColor(collapsedShadowColor), fraction); textPaint.setShadowLayer( - lerp(expandedShadowRadius, collapsedShadowRadius, fraction, null), - lerp(expandedShadowDx, collapsedShadowDx, fraction, null), - lerp(expandedShadowDy, collapsedShadowDy, fraction, null), - blendColors( - getCurrentColor(expandedShadowColor), getCurrentColor(collapsedShadowColor), fraction)); + currentShadowRadius, currentShadowDx, currentShadowDy, currentShadowColor); if (fadeModeEnabled) { int originalAlpha = textPaint.getAlpha(); @@ -659,8 +666,7 @@ private void calculateOffsets(final float fraction) { ViewCompat.postInvalidateOnAnimation(view); } - private float calculateFadeModeTextAlpha( - @FloatRange(from = 0.0, to = 1.0) float fraction) { + private float calculateFadeModeTextAlpha(@FloatRange(from = 0.0, to = 1.0) float fraction) { if (fraction <= fadeModeThresholdFraction) { return AnimationUtils.lerp( /* startValue= */ 1, @@ -870,10 +876,29 @@ private void drawMultilineTransition(@NonNull Canvas canvas, float currentExpand canvas.translate(currentExpandedX, y); // Expanded text textPaint.setAlpha((int) (expandedTextBlend * originalAlpha)); + // Workaround for API 31(+). Paint applies an inverse alpha of Paint object on the shadow layer + // when collapsing mode is scale and shadow color is opaque. The workaround is to set the shadow + // not opaque. Then Paint will respect to the color's alpha. Applying the shadow color for + // expanded text. + if (VERSION.SDK_INT >= VERSION_CODES.S) { + textPaint.setShadowLayer( + currentShadowRadius, + currentShadowDx, + currentShadowDy, + MaterialColors.compositeARGBWithAlpha(currentShadowColor, textPaint.getAlpha())); + } textLayout.draw(canvas); // Collapsed text textPaint.setAlpha((int) (collapsedTextBlend * originalAlpha)); + // Workaround for API 31(+). Applying the shadow color for collapsed texct. + if (VERSION.SDK_INT >= VERSION_CODES.S) { + textPaint.setShadowLayer( + currentShadowRadius, + currentShadowDx, + currentShadowDy, + MaterialColors.compositeARGBWithAlpha(currentShadowColor, textPaint.getAlpha())); + } int lineBaseline = textLayout.getLineBaseline(0); canvas.drawText( textToDrawCollapsed, @@ -882,6 +907,13 @@ private void drawMultilineTransition(@NonNull Canvas canvas, float currentExpand /* x = */ 0, lineBaseline, textPaint); + // Reverse workaround for API 31(+). Applying opaque shadow color after the expanded text and + // the collapsed text are drawn. + if (VERSION.SDK_INT >= VERSION_CODES.S) { + textPaint.setShadowLayer( + currentShadowRadius, currentShadowDx, currentShadowDy, currentShadowColor); + } + if (!fadeModeEnabled) { // Remove ellipsis for Cross-section animation String tmp = textToDrawCollapsed.toString().trim(); @@ -991,9 +1023,10 @@ private void calculateUsingTextSize(final float fraction, boolean forceRecalcula // the collapsed width // Otherwise we'll just use the expanded width - availableWidth = scaledDownWidth > collapsedWidth - ? min(collapsedWidth / textSizeRatio, expandedWidth) - : expandedWidth; + availableWidth = + scaledDownWidth > collapsedWidth + ? min(collapsedWidth / textSizeRatio, expandedWidth) + : expandedWidth; } } @@ -1197,18 +1230,27 @@ public ColorStateList getCollapsedTextColor() { } /** - * Blend {@code color1} and {@code color2} using the given ratio. + * Blend between two ARGB colors using the given ratio. + * + *

A blend ratio of 0.0 will result in {@code color1}, 0.5 will give an even blend, 1.0 will + * result in {@code color2}. * - * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend, - * 1.0 will return {@code color2}. + *

This is different from the AndroidX implementation by rounding the blended channel values + * with {@link Math#round(float)}. + * + * @param color1 the first ARGB color + * @param color2 the second ARGB color + * @param ratio the blend ratio of {@code color1} to {@code color2} */ - private static int blendColors(int color1, int color2, float ratio) { - final float inverseRatio = 1f - ratio; - float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio); - float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio); - float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio); - float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio); - return Color.argb((int) a, (int) r, (int) g, (int) b); + @ColorInt + private static int blendARGB( + @ColorInt int color1, @ColorInt int color2, @FloatRange(from = 0.0, to = 1.0) float ratio) { + final float inverseRatio = 1 - ratio; + float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio; + float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio; + float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio; + float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio; + return Color.argb(Math.round(a), Math.round(r), Math.round(g), Math.round(b)); } private static float lerp(