1 /*
2  * Copyright (C) 2020 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.launcher3.views;
17 
18 import static com.android.app.animation.Interpolators.LINEAR;
19 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
20 import static com.android.launcher3.Utilities.boundToRange;
21 import static com.android.launcher3.Utilities.mapToRange;
22 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
23 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
24 
25 import static java.lang.Math.max;
26 
27 import android.animation.ValueAnimator;
28 import android.content.Context;
29 import android.graphics.Canvas;
30 import android.graphics.Color;
31 import android.graphics.Outline;
32 import android.graphics.Path;
33 import android.graphics.Rect;
34 import android.graphics.RectF;
35 import android.graphics.drawable.AdaptiveIconDrawable;
36 import android.graphics.drawable.ColorDrawable;
37 import android.graphics.drawable.Drawable;
38 import android.util.AttributeSet;
39 import android.view.View;
40 import android.view.ViewGroup.MarginLayoutParams;
41 import android.view.ViewOutlineProvider;
42 
43 import androidx.annotation.Nullable;
44 import androidx.core.util.Consumer;
45 
46 import com.android.launcher3.DeviceProfile;
47 import com.android.launcher3.R;
48 import com.android.launcher3.Utilities;
49 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
50 import com.android.launcher3.graphics.IconShape;
51 
52 /**
53  * A view used to draw both layers of an {@link AdaptiveIconDrawable}.
54  * Supports springing just the foreground layer.
55  * Supports clipping the icon to/from its icon shape.
56  */
57 public class ClipIconView extends View implements ClipPathView {
58 
59     private static final Rect sTmpRect = new Rect();
60 
61     private final int mBlurSizeOutline;
62     private final boolean mIsRtl;
63 
64     private @Nullable Drawable mForeground;
65     private @Nullable Drawable mBackground;
66 
67     private boolean mIsAdaptiveIcon = false;
68 
69     private ValueAnimator mRevealAnimator;
70 
71     private final Rect mStartRevealRect = new Rect();
72     private final Rect mEndRevealRect = new Rect();
73     private Path mClipPath;
74     private float mTaskCornerRadius;
75 
76     private final Rect mOutline = new Rect();
77     private final Rect mFinalDrawableBounds = new Rect();
78 
79     @Nullable private TaskViewArtist mTaskViewArtist;
80 
ClipIconView(Context context)81     public ClipIconView(Context context) {
82         this(context, null);
83     }
84 
ClipIconView(Context context, AttributeSet attrs)85     public ClipIconView(Context context, AttributeSet attrs) {
86         this(context, attrs, 0);
87     }
88 
ClipIconView(Context context, AttributeSet attrs, int defStyleAttr)89     public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
90         super(context, attrs, defStyleAttr);
91         mBlurSizeOutline = getResources().getDimensionPixelSize(
92                 R.dimen.blur_size_medium_outline);
93         mIsRtl = Utilities.isRtl(getResources());
94     }
95 
96     /**
97      * Sets a {@link TaskViewArtist} that will draw a {@link com.android.quickstep.views.TaskView}
98      * within the clip bounds of this view.
99      */
setTaskViewArtist(TaskViewArtist taskViewArtist)100     public void setTaskViewArtist(TaskViewArtist taskViewArtist) {
101         if (!enableAdditionalHomeAnimations()) {
102             return;
103         }
104         mTaskViewArtist = taskViewArtist;
105         invalidate();
106     }
107 
108     /**
109      * Update the icon UI to match the provided parameters during an animation frame
110      */
update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, View container, DeviceProfile dp)111     public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
112             boolean isOpening, View container, DeviceProfile dp) {
113         update(rect, progress, shapeProgressStart, cornerRadius, isOpening, container, dp, 255);
114     }
115 
116     /**
117      * Update the icon UI to match the provided parameters during an animation frame, optionally
118      * varying the alpha of the {@link TaskViewArtist}
119      */
update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, View container, DeviceProfile dp, int taskViewDrawAlpha)120     public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
121             boolean isOpening, View container, DeviceProfile dp, int taskViewDrawAlpha) {
122         MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams();
123 
124         float dX = mIsRtl
125                 ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
126                 : rect.left - lp.getMarginStart();
127         float dY = rect.top - lp.topMargin;
128         container.setTranslationX(dX);
129         container.setTranslationY(dY);
130 
131         float minSize = Math.min(lp.width, lp.height);
132         float scaleX = rect.width() / minSize;
133         float scaleY = rect.height() / minSize;
134         float scale = Math.max(1f, Math.min(scaleX, scaleY));
135         if (mTaskViewArtist != null) {
136             mTaskViewArtist.taskViewDrawWidth = lp.width;
137             mTaskViewArtist.taskViewDrawHeight = lp.height;
138             mTaskViewArtist.taskViewDrawAlpha = taskViewDrawAlpha;
139             mTaskViewArtist.taskViewDrawScale = (mTaskViewArtist.drawForPortraitLayout
140                     ? Math.min(lp.height, lp.width) : Math.max(lp.height, lp.width))
141                     / mTaskViewArtist.taskViewMinSize;
142         }
143 
144         if (Float.isNaN(scale) || Float.isInfinite(scale)) {
145             // Views are no longer laid out, do not update.
146             return;
147         }
148 
149         update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale, minSize, dp);
150 
151         container.setPivotX(0);
152         container.setPivotY(0);
153         container.setScaleX(scale);
154         container.setScaleY(scale);
155 
156         container.invalidate();
157     }
158 
update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, float scale, float minSize, DeviceProfile dp)159     private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
160             boolean isOpening, float scale, float minSize, DeviceProfile dp) {
161         // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
162         float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
163 
164         float shapeRevealProgress = boundToRange(mapToRange(max(shapeProgressStart, progress),
165                 shapeProgressStart, 1f, 0, toMax, LINEAR), 0, 1);
166 
167         if (dp.isLandscape) {
168             mOutline.right = (int) (rect.width() / scale);
169         } else {
170             mOutline.bottom = (int) (rect.height() / scale);
171         }
172 
173         mTaskCornerRadius = cornerRadius / scale;
174         if (mIsAdaptiveIcon) {
175             if (!isOpening && progress >= shapeProgressStart) {
176                 if (mRevealAnimator == null) {
177                     mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
178                             .createRevealAnimator(this, mStartRevealRect,
179                                     mOutline, mTaskCornerRadius, !isOpening);
180                     mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
181                     mRevealAnimator.start();
182                     // We pause here so we can set the current fraction ourselves.
183                     mRevealAnimator.pause();
184                 }
185                 mRevealAnimator.setCurrentFraction(shapeRevealProgress);
186             }
187 
188             float drawableScale = (dp.isLandscape ? mOutline.width() : mOutline.height())
189                     / minSize;
190             setBackgroundDrawableBounds(drawableScale, dp.isLandscape);
191 
192             // Center align foreground
193             int height = mFinalDrawableBounds.height();
194             int width = mFinalDrawableBounds.width();
195             int diffY = dp.isLandscape ? 0
196                     : (int) (((height * drawableScale) - height) / 2);
197             int diffX = dp.isLandscape ? (int) (((width * drawableScale) - width) / 2)
198                     : 0;
199             sTmpRect.set(mFinalDrawableBounds);
200             sTmpRect.offset(diffX, diffY);
201             mForeground.setBounds(sTmpRect);
202         }
203         invalidate();
204         invalidateOutline();
205     }
206 
setBackgroundDrawableBounds(float scale, boolean isLandscape)207     private void setBackgroundDrawableBounds(float scale, boolean isLandscape) {
208         sTmpRect.set(mFinalDrawableBounds);
209         Utilities.scaleRectAboutCenter(sTmpRect, scale);
210         // Since the drawable is at the top of the view, we need to offset to keep it centered.
211         if (isLandscape) {
212             sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top);
213         } else {
214             sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale));
215         }
216         mBackground.setBounds(sTmpRect);
217     }
218 
endReveal()219     protected void endReveal() {
220         if (mRevealAnimator != null) {
221             mRevealAnimator.end();
222         }
223     }
224 
225     /**
226      * Sets the icon for this view as part of initial setup
227      */
setIcon(@ullable Drawable drawable, int iconOffset, MarginLayoutParams lp, boolean isOpening, DeviceProfile dp)228     public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp,
229             boolean isOpening, DeviceProfile dp) {
230         mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
231         if (mIsAdaptiveIcon) {
232             boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
233 
234             AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
235             Drawable background = adaptiveIcon.getBackground();
236             if (background == null) {
237                 background = new ColorDrawable(Color.TRANSPARENT);
238             }
239             mBackground = background;
240             Drawable foreground = adaptiveIcon.getForeground();
241             if (foreground == null) {
242                 foreground = new ColorDrawable(Color.TRANSPARENT);
243             }
244             mForeground = foreground;
245 
246             final int originalHeight = lp.height;
247             final int originalWidth = lp.width;
248 
249             int blurMargin = mBlurSizeOutline / 2;
250             mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
251 
252             if (!isFolderIcon) {
253                 mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
254             }
255             mForeground.setBounds(mFinalDrawableBounds);
256             mBackground.setBounds(mFinalDrawableBounds);
257 
258             mStartRevealRect.set(0, 0, originalWidth, originalHeight);
259 
260             if (!isFolderIcon) {
261                 Utilities.scaleRectAboutCenter(mStartRevealRect,
262                         IconShape.INSTANCE.get(getContext()).getNormalizationScale());
263             }
264 
265             if (dp.isLandscape) {
266                 lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio);
267             } else {
268                 lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio);
269             }
270 
271             int left = mIsRtl
272                     ? dp.widthPx - lp.getMarginStart() - lp.width
273                     : lp.leftMargin;
274             layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
275 
276             float scale = Math.max((float) lp.height / originalHeight,
277                     (float) lp.width / originalWidth);
278             float bgDrawableStartScale;
279             if (isOpening) {
280                 bgDrawableStartScale = 1f;
281                 mOutline.set(0, 0, originalWidth, originalHeight);
282             } else {
283                 bgDrawableStartScale = scale;
284                 mOutline.set(0, 0, lp.width, lp.height);
285             }
286             setBackgroundDrawableBounds(bgDrawableStartScale, dp.isLandscape);
287             mEndRevealRect.set(0, 0, lp.width, lp.height);
288             setOutlineProvider(new ViewOutlineProvider() {
289                 @Override
290                 public void getOutline(View view, Outline outline) {
291                     outline.setRoundRect(mOutline, mTaskCornerRadius);
292                 }
293             });
294             setClipToOutline(true);
295         } else {
296             setBackground(drawable);
297             setClipToOutline(false);
298         }
299 
300         invalidate();
301         invalidateOutline();
302     }
303 
304     @Override
setClipPath(Path clipPath)305     public void setClipPath(Path clipPath) {
306         mClipPath = clipPath;
307         invalidate();
308     }
309 
310     @Override
draw(Canvas canvas)311     public void draw(Canvas canvas) {
312         int count = canvas.save();
313         if (mClipPath != null) {
314             canvas.clipPath(mClipPath);
315         }
316         super.draw(canvas);
317         if (mBackground != null) {
318             mBackground.draw(canvas);
319         }
320         if (mForeground != null) {
321             mForeground.draw(canvas);
322         }
323         if (mTaskViewArtist != null) {
324             canvas.saveLayerAlpha(
325                     0,
326                     0,
327                     mTaskViewArtist.taskViewDrawWidth,
328                     mTaskViewArtist.taskViewDrawHeight,
329                     mTaskViewArtist.taskViewDrawAlpha);
330             float drawScale = mTaskViewArtist.taskViewDrawScale;
331             canvas.translate(drawScale * mTaskViewArtist.taskViewTranslationX,
332                     drawScale * mTaskViewArtist.taskViewTranslationY);
333             canvas.scale(drawScale, drawScale);
334             mTaskViewArtist.taskViewDrawCallback.accept(canvas);
335         }
336         canvas.restoreToCount(count);
337     }
338 
recycle()339     void recycle() {
340         setBackground(null);
341         mIsAdaptiveIcon = false;
342         mForeground = null;
343         mBackground = null;
344         mClipPath = null;
345         mFinalDrawableBounds.setEmpty();
346         if (mRevealAnimator != null) {
347             mRevealAnimator.cancel();
348         }
349         mRevealAnimator = null;
350         mTaskCornerRadius = 0;
351         mOutline.setEmpty();
352         mTaskViewArtist = null;
353     }
354 
355     /**
356      * Utility class to help draw a {@link com.android.quickstep.views.TaskView} within
357      * a {@link ClipIconView} bounds.
358      */
359     public static class TaskViewArtist {
360 
361         public final Consumer<Canvas> taskViewDrawCallback;
362         public final float taskViewTranslationX;
363         public final float taskViewTranslationY;
364         public final float taskViewMinSize;
365         public final boolean drawForPortraitLayout;
366 
367         public int taskViewDrawAlpha;
368         public float taskViewDrawScale;
369         public int taskViewDrawWidth;
370         public int taskViewDrawHeight;
371 
TaskViewArtist( Consumer<Canvas> taskViewDrawCallback, float taskViewTranslationX, float taskViewTranslationY, float taskViewMinSize, boolean drawForPortraitLayout)372         public TaskViewArtist(
373                 Consumer<Canvas> taskViewDrawCallback,
374                 float taskViewTranslationX,
375                 float taskViewTranslationY,
376                 float taskViewMinSize,
377                 boolean drawForPortraitLayout) {
378             this.taskViewDrawCallback = taskViewDrawCallback;
379             this.taskViewTranslationX = taskViewTranslationX;
380             this.taskViewTranslationY = taskViewTranslationY;
381             this.taskViewMinSize = taskViewMinSize;
382             this.drawForPortraitLayout = drawForPortraitLayout;
383         }
384     }
385 }
386