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