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