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 36 import java.util.stream.IntStream; 37 38 /** 39 * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a 40 * an App Widget activity launch animation. 41 */ 42 @TargetApi(Build.VERSION_CODES.S) 43 final class FloatingWidgetBackgroundView extends View { 44 private final ColorDrawable mFallbackDrawable = new ColorDrawable(); 45 private final DrawableProperties mForegroundProperties = new DrawableProperties(); 46 private final DrawableProperties mBackgroundProperties = new DrawableProperties(); 47 48 @Nullable 49 private Drawable mOriginalForeground; 50 @Nullable 51 private Drawable mOriginalBackground; 52 private float mFinalRadius; 53 private float mInitialOutlineRadius; 54 private float mOutlineRadius; 55 private boolean mIsUsingFallback; 56 private View mSourceView; 57 FloatingWidgetBackgroundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)58 FloatingWidgetBackgroundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 59 super(context, attrs, defStyleAttr); 60 setOutlineProvider(new ViewOutlineProvider() { 61 @Override 62 public void getOutline(View view, Outline outline) { 63 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius); 64 } 65 }); 66 setClipToOutline(true); 67 } 68 init(LauncherAppWidgetHostView hostView, @NonNull View backgroundView, float finalRadius, int fallbackBackgroundColor)69 void init(LauncherAppWidgetHostView hostView, @NonNull View backgroundView, 70 float finalRadius, int fallbackBackgroundColor) { 71 mFinalRadius = finalRadius; 72 mSourceView = backgroundView; 73 mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView); 74 mIsUsingFallback = false; 75 if (isSupportedDrawable(backgroundView.getForeground())) { 76 if (backgroundView.getTag(R.id.saved_floating_widget_foreground) == null) { 77 mOriginalForeground = backgroundView.getForeground(); 78 backgroundView.setTag(R.id.saved_floating_widget_foreground, mOriginalForeground); 79 } else { 80 mOriginalForeground = (Drawable) backgroundView.getTag( 81 R.id.saved_floating_widget_foreground); 82 } 83 mForegroundProperties.init( 84 mOriginalForeground.getConstantState().newDrawable().mutate()); 85 setForeground(mForegroundProperties.mDrawable); 86 Drawable clipPlaceholder = 87 mOriginalForeground.getConstantState().newDrawable().mutate(); 88 clipPlaceholder.setAlpha(0); 89 mSourceView.setForeground(clipPlaceholder); 90 } 91 if (isSupportedDrawable(backgroundView.getBackground())) { 92 if (backgroundView.getTag(R.id.saved_floating_widget_background) == null) { 93 mOriginalBackground = backgroundView.getBackground(); 94 backgroundView.setTag(R.id.saved_floating_widget_background, mOriginalBackground); 95 } else { 96 mOriginalBackground = (Drawable) backgroundView.getTag( 97 R.id.saved_floating_widget_background); 98 } 99 mBackgroundProperties.init( 100 mOriginalBackground.getConstantState().newDrawable().mutate()); 101 setBackground(mBackgroundProperties.mDrawable); 102 Drawable clipPlaceholder = 103 mOriginalBackground.getConstantState().newDrawable().mutate(); 104 clipPlaceholder.setAlpha(0); 105 mSourceView.setBackground(clipPlaceholder); 106 } else if (mOriginalForeground == null) { 107 mFallbackDrawable.setColor(fallbackBackgroundColor); 108 setBackground(mFallbackDrawable); 109 mIsUsingFallback = true; 110 } 111 } 112 113 /** Update the animated properties of the drawables. */ update(float cornerRadiusProgress, float fallbackAlpha)114 void update(float cornerRadiusProgress, float fallbackAlpha) { 115 if (isUninitialized()) return; 116 mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius) 117 * cornerRadiusProgress; 118 mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress); 119 mBackgroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress); 120 setAlpha(mIsUsingFallback ? fallbackAlpha : 1f); 121 } 122 123 /** Restores the drawables to the source view. */ finish()124 void finish() { 125 if (isUninitialized()) return; 126 if (mOriginalForeground != null) mSourceView.setForeground(mOriginalForeground); 127 if (mOriginalBackground != null) mSourceView.setBackground(mOriginalBackground); 128 } 129 recycle()130 void recycle() { 131 if (mSourceView != null) { 132 mSourceView.setTag(R.id.saved_floating_widget_foreground, null); 133 mSourceView.setTag(R.id.saved_floating_widget_background, null); 134 } 135 mSourceView = null; 136 mOriginalForeground = null; 137 mOriginalBackground = null; 138 mOutlineRadius = 0; 139 mFinalRadius = 0; 140 setForeground(null); 141 setBackground(null); 142 } 143 144 /** Get the largest of drawable corner radii or background view outline radius. */ getMaximumRadius()145 float getMaximumRadius() { 146 if (isUninitialized()) return 0; 147 return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground), 148 getMaxRadius(mOriginalBackground))); 149 } 150 isUninitialized()151 private boolean isUninitialized() { 152 return mSourceView == null; 153 } 154 155 /** Returns the maximum corner radius of {@param drawable}. */ getMaxRadius(@ullable Drawable drawable)156 private static float getMaxRadius(@Nullable Drawable drawable) { 157 if (!(drawable instanceof GradientDrawable)) return 0; 158 float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii(); 159 float cornerRadius = ((GradientDrawable) drawable).getCornerRadius(); 160 double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length) 161 .mapToDouble(i -> cornerRadii[i]).max().orElse(0); 162 return Math.max(cornerRadius, (float) radiiMax); 163 } 164 165 /** Returns whether the given drawable type is supported. */ isSupportedDrawable(Drawable drawable)166 private static boolean isSupportedDrawable(Drawable drawable) { 167 return drawable instanceof ColorDrawable || (drawable instanceof GradientDrawable 168 && ((GradientDrawable) drawable).getShape() == GradientDrawable.RECTANGLE); 169 } 170 171 /** Corner radius from source view's outline, or enforced view. */ getOutlineRadius(LauncherAppWidgetHostView hostView, View v)172 private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) { 173 if (hostView.hasEnforcedCornerRadius()) { 174 return hostView.getEnforcedCornerRadius(); 175 } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider 176 && v.getClipToOutline()) { 177 return ((RemoteViewOutlineProvider) v.getOutlineProvider()).getRadius(); 178 } 179 return 0; 180 } 181 182 /** Stores and modifies a drawable's properties through an animation. */ 183 private static class DrawableProperties { 184 @Nullable 185 private Drawable mDrawable; 186 private float mOriginalRadius; 187 @Nullable 188 private float[] mOriginalRadii; 189 private final float[] mTmpRadii = new float[8]; 190 191 /** Store a drawable's animated properties. */ init(Drawable drawable)192 void init(Drawable drawable) { 193 mDrawable = drawable; 194 if (!(drawable instanceof GradientDrawable)) return; 195 mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius(); 196 mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii(); 197 } 198 199 /** 200 * Update the drawable for the given animation state. 201 * 202 * @param finalRadius the radius of each corner when {@param progress} is 1 203 * @param progress the linear progress of the corner radius from its original value to 204 * {@param finalRadius} 205 */ updateDrawable(float finalRadius, float progress)206 void updateDrawable(float finalRadius, float progress) { 207 if (!(mDrawable instanceof GradientDrawable)) return; 208 GradientDrawable d = (GradientDrawable) mDrawable; 209 if (mOriginalRadii != null) { 210 for (int i = 0; i < mOriginalRadii.length; i++) { 211 mTmpRadii[i] = mOriginalRadii[i] + (finalRadius - mOriginalRadii[i]) * progress; 212 } 213 d.setCornerRadii(mTmpRadii); 214 } else { 215 d.setCornerRadius(mOriginalRadius + (finalRadius - mOriginalRadius) * progress); 216 } 217 } 218 } 219 } 220