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