1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.quickstep.views; 17 18 import android.annotation.TargetApi; 19 import android.content.Context; 20 import android.graphics.Outline; 21 import android.graphics.drawable.ColorDrawable; 22 import android.graphics.drawable.Drawable; 23 import android.graphics.drawable.GradientDrawable; 24 import android.os.Build; 25 import android.util.AttributeSet; 26 import android.view.View; 27 import android.view.ViewOutlineProvider; 28 import android.widget.RemoteViews.RemoteViewOutlineProvider; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 33 import com.android.launcher3.R; 34 import com.android.launcher3.widget.LauncherAppWidgetHostView; 35 import com.android.launcher3.widget.RoundedCornerEnforcement; 36 37 import java.util.stream.IntStream; 38 39 /** 40 * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a 41 * an App Widget activity launch animation. 42 */ 43 @TargetApi(Build.VERSION_CODES.S) 44 final class FloatingWidgetBackgroundView extends View { 45 private final ColorDrawable mFallbackDrawable = new ColorDrawable(); 46 private final DrawableProperties mForegroundProperties = new DrawableProperties(); 47 private final DrawableProperties mBackgroundProperties = new DrawableProperties(); 48 49 @Nullable 50 private Drawable mOriginalForeground; 51 @Nullable 52 private Drawable mOriginalBackground; 53 private float mFinalRadius; 54 private float mInitialOutlineRadius; 55 private float mOutlineRadius; 56 private boolean mIsUsingFallback; 57 private View mSourceView; 58 FloatingWidgetBackgroundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)59 FloatingWidgetBackgroundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 60 super(context, attrs, defStyleAttr); 61 setOutlineProvider(new ViewOutlineProvider() { 62 @Override 63 public void getOutline(View view, Outline outline) { 64 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius); 65 } 66 }); 67 setClipToOutline(true); 68 } 69 init(LauncherAppWidgetHostView hostView, @NonNull View backgroundView, float finalRadius, int fallbackBackgroundColor)70 void init(LauncherAppWidgetHostView hostView, @NonNull View backgroundView, 71 float finalRadius, int fallbackBackgroundColor) { 72 mFinalRadius = finalRadius; 73 mSourceView = backgroundView; 74 mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView); 75 mIsUsingFallback = false; 76 if (isSupportedDrawable(backgroundView.getForeground())) { 77 if (backgroundView.getTag(R.id.saved_floating_widget_foreground) == null) { 78 mOriginalForeground = backgroundView.getForeground(); 79 backgroundView.setTag(R.id.saved_floating_widget_foreground, mOriginalForeground); 80 } else { 81 mOriginalForeground = (Drawable) backgroundView.getTag( 82 R.id.saved_floating_widget_foreground); 83 } 84 mForegroundProperties.init( 85 mOriginalForeground.getConstantState().newDrawable().mutate()); 86 setForeground(mForegroundProperties.mDrawable); 87 Drawable clipPlaceholder = 88 mOriginalForeground.getConstantState().newDrawable().mutate(); 89 clipPlaceholder.setAlpha(0); 90 mSourceView.setForeground(clipPlaceholder); 91 } 92 if (isSupportedDrawable(backgroundView.getBackground())) { 93 if (backgroundView.getTag(R.id.saved_floating_widget_background) == null) { 94 mOriginalBackground = backgroundView.getBackground(); 95 backgroundView.setTag(R.id.saved_floating_widget_background, mOriginalBackground); 96 } else { 97 mOriginalBackground = (Drawable) backgroundView.getTag( 98 R.id.saved_floating_widget_background); 99 } 100 mBackgroundProperties.init( 101 mOriginalBackground.getConstantState().newDrawable().mutate()); 102 setBackground(mBackgroundProperties.mDrawable); 103 Drawable clipPlaceholder = 104 mOriginalBackground.getConstantState().newDrawable().mutate(); 105 clipPlaceholder.setAlpha(0); 106 mSourceView.setBackground(clipPlaceholder); 107 } else if (mOriginalForeground == null) { 108 mFallbackDrawable.setColor(fallbackBackgroundColor); 109 setBackground(mFallbackDrawable); 110 mIsUsingFallback = true; 111 } 112 } 113 114 /** Update the animated properties of the drawables. */ update(float cornerRadiusProgress, float fallbackAlpha)115 void update(float cornerRadiusProgress, float fallbackAlpha) { 116 if (isUninitialized()) return; 117 mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius) 118 * cornerRadiusProgress; 119 mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress); 120 mBackgroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress); 121 setAlpha(mIsUsingFallback ? fallbackAlpha : 1f); 122 } 123 124 /** Restores the drawables to the source view. */ finish()125 void finish() { 126 if (isUninitialized()) return; 127 if (mOriginalForeground != null) mSourceView.setForeground(mOriginalForeground); 128 if (mOriginalBackground != null) mSourceView.setBackground(mOriginalBackground); 129 } 130 recycle()131 void recycle() { 132 if (mSourceView != null) { 133 mSourceView.setTag(R.id.saved_floating_widget_foreground, null); 134 mSourceView.setTag(R.id.saved_floating_widget_background, null); 135 } 136 mSourceView = null; 137 mOriginalForeground = null; 138 mOriginalBackground = null; 139 mOutlineRadius = 0; 140 mFinalRadius = 0; 141 setForeground(null); 142 setBackground(null); 143 } 144 145 /** Get the largest of drawable corner radii or background view outline radius. */ getMaximumRadius()146 float getMaximumRadius() { 147 if (isUninitialized()) return 0; 148 return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground), 149 getMaxRadius(mOriginalBackground))); 150 } 151 isUninitialized()152 private boolean isUninitialized() { 153 return mSourceView == null; 154 } 155 156 /** Returns the maximum corner radius of {@param drawable}. */ getMaxRadius(@ullable Drawable drawable)157 private static float getMaxRadius(@Nullable Drawable drawable) { 158 if (!(drawable instanceof GradientDrawable)) return 0; 159 float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii(); 160 float cornerRadius = ((GradientDrawable) drawable).getCornerRadius(); 161 double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length) 162 .mapToDouble(i -> cornerRadii[i]).max().orElse(0); 163 return Math.max(cornerRadius, (float) radiiMax); 164 } 165 166 /** Returns whether the given drawable type is supported. */ isSupportedDrawable(Drawable drawable)167 private static boolean isSupportedDrawable(Drawable drawable) { 168 return drawable instanceof ColorDrawable || (drawable instanceof GradientDrawable 169 && ((GradientDrawable) drawable).getShape() == GradientDrawable.RECTANGLE); 170 } 171 172 /** Corner radius from source view's outline, or enforced view. */ getOutlineRadius(LauncherAppWidgetHostView hostView, View v)173 private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) { 174 if (RoundedCornerEnforcement.isRoundedCornerEnabled() 175 && hostView.hasEnforcedCornerRadius()) { 176 return hostView.getEnforcedCornerRadius(); 177 } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider 178 && v.getClipToOutline()) { 179 return ((RemoteViewOutlineProvider) v.getOutlineProvider()).getRadius(); 180 } 181 return 0; 182 } 183 184 /** Stores and modifies a drawable's properties through an animation. */ 185 private static class DrawableProperties { 186 @Nullable 187 private Drawable mDrawable; 188 private float mOriginalRadius; 189 @Nullable 190 private float[] mOriginalRadii; 191 private final float[] mTmpRadii = new float[8]; 192 193 /** Store a drawable's animated properties. */ init(Drawable drawable)194 void init(Drawable drawable) { 195 mDrawable = drawable; 196 if (!(drawable instanceof GradientDrawable)) return; 197 mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius(); 198 mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii(); 199 } 200 201 /** 202 * Update the drawable for the given animation state. 203 * 204 * @param finalRadius the radius of each corner when {@param progress} is 1 205 * @param progress the linear progress of the corner radius from its original value to 206 * {@param finalRadius} 207 */ updateDrawable(float finalRadius, float progress)208 void updateDrawable(float finalRadius, float progress) { 209 if (!(mDrawable instanceof GradientDrawable)) return; 210 GradientDrawable d = (GradientDrawable) mDrawable; 211 if (mOriginalRadii != null) { 212 for (int i = 0; i < mOriginalRadii.length; i++) { 213 mTmpRadii[i] = mOriginalRadii[i] + (finalRadius - mOriginalRadii[i]) * progress; 214 } 215 d.setCornerRadii(mTmpRadii); 216 } else { 217 d.setCornerRadius(mOriginalRadius + (finalRadius - mOriginalRadius) * progress); 218 } 219 } 220 } 221 } 222