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.animation.Animator;
19 import android.animation.Animator.AnimatorListener;
20 import android.annotation.TargetApi;
21 import android.content.Context;
22 import android.graphics.Matrix;
23 import android.graphics.RectF;
24 import android.os.Build;
25 import android.util.AttributeSet;
26 import android.util.Size;
27 import android.view.GhostView;
28 import android.view.RemoteAnimationTarget;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
32 import android.widget.FrameLayout;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.launcher3.R;
37 import com.android.launcher3.Utilities;
38 import com.android.launcher3.dragndrop.DragLayer;
39 import com.android.launcher3.uioverrides.QuickstepLauncher;
40 import com.android.launcher3.util.Themes;
41 import com.android.launcher3.views.FloatingView;
42 import com.android.launcher3.views.ListenerView;
43 import com.android.launcher3.widget.LauncherAppWidgetHostView;
44 import com.android.launcher3.widget.RoundedCornerEnforcement;
45 
46 /** A view that mimics an App Widget through a launch animation. */
47 @TargetApi(Build.VERSION_CODES.S)
48 public class FloatingWidgetView extends FrameLayout implements AnimatorListener,
49         OnGlobalLayoutListener, FloatingView {
50     private static final Matrix sTmpMatrix = new Matrix();
51 
52     private final QuickstepLauncher mLauncher;
53     private final ListenerView mListenerView;
54     private final FloatingWidgetBackgroundView mBackgroundView;
55     private final RectF mBackgroundOffset = new RectF();
56 
57     private LauncherAppWidgetHostView mAppWidgetView;
58     private View mAppWidgetBackgroundView;
59     private RectF mBackgroundPosition;
60     @Nullable
61     private GhostView mForegroundOverlayView;
62 
63     @Nullable
64     private Runnable mEndRunnable;
65     @Nullable
66     private Runnable mFastFinishRunnable;
67     @Nullable
68     private Runnable mOnTargetChangeRunnable;
69     private boolean mAppTargetIsTranslucent;
70 
71     private float mIconOffsetY;
72 
FloatingWidgetView(Context context)73     public FloatingWidgetView(Context context) {
74         this(context, null);
75     }
76 
FloatingWidgetView(Context context, @Nullable AttributeSet attrs)77     public FloatingWidgetView(Context context, @Nullable AttributeSet attrs) {
78         this(context, attrs, 0);
79     }
80 
FloatingWidgetView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)81     public FloatingWidgetView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
82         super(context, attrs, defStyleAttr);
83         mLauncher = QuickstepLauncher.getLauncher(context);
84         mListenerView = new ListenerView(context, attrs);
85         mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr);
86         addView(mBackgroundView);
87         setWillNotDraw(false);
88     }
89 
90     @Override
onAnimationEnd(Animator animator)91     public void onAnimationEnd(Animator animator) {
92         Runnable endRunnable = mEndRunnable;
93         mEndRunnable = null;
94         if (endRunnable != null) {
95             endRunnable.run();
96         }
97     }
98 
99     @Override
onAnimationStart(Animator animator)100     public void onAnimationStart(Animator animator) {
101     }
102 
103     @Override
onAnimationCancel(Animator animator)104     public void onAnimationCancel(Animator animator) {
105     }
106 
107     @Override
onAnimationRepeat(Animator animator)108     public void onAnimationRepeat(Animator animator) {
109     }
110 
111     @Override
onAttachedToWindow()112     protected void onAttachedToWindow() {
113         super.onAttachedToWindow();
114         getViewTreeObserver().addOnGlobalLayoutListener(this);
115     }
116 
117     @Override
onDetachedFromWindow()118     protected void onDetachedFromWindow() {
119         getViewTreeObserver().removeOnGlobalLayoutListener(this);
120         super.onDetachedFromWindow();
121     }
122 
123     @Override
onGlobalLayout()124     public void onGlobalLayout() {
125         if (isUninitialized()) return;
126         positionViews();
127         if (mOnTargetChangeRunnable != null) {
128             mOnTargetChangeRunnable.run();
129         }
130     }
131 
132     /** Sets a runnable that is called on global layout change. */
setOnTargetChangeListener(Runnable onTargetChangeListener)133     public void setOnTargetChangeListener(Runnable onTargetChangeListener) {
134         mOnTargetChangeRunnable = onTargetChangeListener;
135     }
136 
137     /** Sets a runnable that is called after a call to {@link #fastFinish()}. */
setFastFinishRunnable(Runnable runnable)138     public void setFastFinishRunnable(Runnable runnable) {
139         mFastFinishRunnable = runnable;
140     }
141 
142     /** Callback at the end or early exit of the animation. */
143     @Override
fastFinish()144     public void fastFinish() {
145         if (isUninitialized()) return;
146         Runnable fastFinishRunnable = mFastFinishRunnable;
147         if (fastFinishRunnable != null) {
148             fastFinishRunnable.run();
149         }
150         Runnable endRunnable = mEndRunnable;
151         mEndRunnable = null;
152         if (endRunnable != null) {
153             endRunnable.run();
154         }
155     }
156 
init(DragLayer dragLayer, LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius, boolean appTargetIsTranslucent, int fallbackBackgroundColor)157     private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView,
158             RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius,
159             boolean appTargetIsTranslucent, int fallbackBackgroundColor) {
160         mAppWidgetView = originalView;
161         // Deferrals must begin before GhostView is created. See b/190818220
162         mAppWidgetView.beginDeferringUpdates();
163         mBackgroundPosition = widgetBackgroundPosition;
164         mAppTargetIsTranslucent = appTargetIsTranslucent;
165         mEndRunnable = () -> finish(dragLayer);
166 
167         mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView);
168         if (mAppWidgetBackgroundView == null) {
169             mAppWidgetBackgroundView = mAppWidgetView;
170         }
171 
172         getRelativePosition(mAppWidgetBackgroundView, dragLayer, mBackgroundPosition);
173         getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset);
174         if (!mAppTargetIsTranslucent) {
175             mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius,
176                     fallbackBackgroundColor);
177             // Layout call before GhostView creation so that the overlaid view isn't clipped
178             layout(0, 0, windowSize.getWidth(), windowSize.getHeight());
179             mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this);
180             positionViews();
181         }
182 
183         mListenerView.setListener(this::fastFinish);
184         dragLayer.addView(mListenerView);
185     }
186 
187     /**
188      * Updates the position and opacity of the floating widget's components.
189      *
190      * @param backgroundPosition      the new position of the widget's background relative to the
191      *                                {@link FloatingWidgetView}'s parent
192      * @param floatingWidgetAlpha     the overall opacity of the {@link FloatingWidgetView}
193      * @param foregroundAlpha         the opacity of the foreground layer
194      * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App
195      *                                Widget doesn't have a background
196      * @param cornerRadiusProgress    progress of the corner radius animation, where 0 is the
197      *                                original radius and 1 is the window radius
198      */
update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha, float fallbackBackgroundAlpha, float cornerRadiusProgress)199     public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha,
200             float fallbackBackgroundAlpha, float cornerRadiusProgress) {
201         if (isUninitialized() || mAppTargetIsTranslucent) return;
202         setAlpha(floatingWidgetAlpha);
203         mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha);
204         mAppWidgetView.setAlpha(foregroundAlpha);
205         mBackgroundPosition = backgroundPosition;
206         positionViews();
207     }
208 
209     @Override
setPositionOffsetY(float y)210     public void setPositionOffsetY(float y) {
211         mIconOffsetY = y;
212         onGlobalLayout();
213     }
214 
215     /** Sets the layout parameters of the floating view and its background view child. */
positionViews()216     private void positionViews() {
217         LayoutParams layoutParams = (LayoutParams) getLayoutParams();
218         layoutParams.setMargins(0, 0, 0, 0);
219         setLayoutParams(layoutParams);
220 
221         // FloatingWidgetView layout is forced LTR
222         mBackgroundView.setTranslationX(mBackgroundPosition.left);
223         mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY);
224         LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
225         backgroundParams.leftMargin = 0;
226         backgroundParams.topMargin = 0;
227         backgroundParams.width = (int) mBackgroundPosition.width();
228         backgroundParams.height = (int) mBackgroundPosition.height();
229         mBackgroundView.setLayoutParams(backgroundParams);
230 
231         if (mForegroundOverlayView != null) {
232             sTmpMatrix.reset();
233             float foregroundScale =
234                     mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
235             sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(),
236                     -mBackgroundOffset.top - mAppWidgetView.getTop());
237             sTmpMatrix.postScale(foregroundScale, foregroundScale);
238             sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top
239                     + mIconOffsetY);
240             mForegroundOverlayView.setMatrix(sTmpMatrix);
241         }
242     }
243 
finish(DragLayer dragLayer)244     private void finish(DragLayer dragLayer) {
245         mAppWidgetView.setAlpha(1f);
246         GhostView.removeGhost(mAppWidgetView);
247         ((ViewGroup) dragLayer.getParent()).removeView(this);
248         dragLayer.removeView(mListenerView);
249         mBackgroundView.finish();
250         // Removing GhostView must occur before ending deferrals. See b/190818220
251         mAppWidgetView.endDeferringUpdates();
252         recycle();
253         mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this);
254     }
255 
getInitialCornerRadius()256     public float getInitialCornerRadius() {
257         return mBackgroundView.getMaximumRadius();
258     }
259 
isUninitialized()260     private boolean isUninitialized() {
261         return mForegroundOverlayView == null;
262     }
263 
recycle()264     private void recycle() {
265         mIconOffsetY = 0;
266         mEndRunnable = null;
267         mFastFinishRunnable = null;
268         mOnTargetChangeRunnable = null;
269         mBackgroundPosition = null;
270         mListenerView.setListener(null);
271         mAppWidgetView = null;
272         mForegroundOverlayView = null;
273         mAppWidgetBackgroundView = null;
274         mBackgroundView.recycle();
275     }
276 
277     /**
278      * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of
279      * {@param originalView}.
280      *
281      * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's
282      *                                 background bounds
283      * @param windowSize               the size of the window when launched
284      * @param windowCornerRadius       the corner radius of the window
285      */
getFloatingWidgetView(QuickstepLauncher launcher, LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius, boolean appTargetsAreTranslucent, int fallbackBackgroundColor)286     public static FloatingWidgetView getFloatingWidgetView(QuickstepLauncher launcher,
287             LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition,
288             Size windowSize, float windowCornerRadius, boolean appTargetsAreTranslucent,
289             int fallbackBackgroundColor) {
290         final DragLayer dragLayer = launcher.getDragLayer();
291         ViewGroup parent = (ViewGroup) dragLayer.getParent();
292         FloatingWidgetView floatingView =
293                 launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent);
294         floatingView.recycle();
295 
296         floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize,
297                 windowCornerRadius, appTargetsAreTranslucent, fallbackBackgroundColor);
298         parent.addView(floatingView);
299         return floatingView;
300     }
301 
302     /**
303      * Extract a background color from a target's task description, or fall back to the given
304      * context's theme background color.
305      */
getDefaultBackgroundColor( Context context, RemoteAnimationTarget target)306     public static int getDefaultBackgroundColor(
307             Context context, RemoteAnimationTarget target) {
308         return (target != null && target.taskInfo != null
309                 && target.taskInfo.taskDescription != null)
310                 ? target.taskInfo.taskDescription.getBackgroundColor()
311                 : Themes.getColorBackground(context);
312     }
313 
getRelativePosition(View descendant, View ancestor, RectF position)314     private static void getRelativePosition(View descendant, View ancestor, RectF position) {
315         float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()};
316         Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points,
317                 false /* includeRootScroll */, true /* ignoreTransform */);
318         position.set(
319                 Math.min(points[0], points[2]),
320                 Math.min(points[1], points[3]),
321                 Math.max(points[0], points[2]),
322                 Math.max(points[1], points[3]));
323     }
324 }
325