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