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.launcher3.Utilities.mapToRange;
19 import static com.android.launcher3.anim.Interpolators.LINEAR;
20 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ValueAnimator;
25 import android.annotation.TargetApi;
26 import android.content.Context;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Outline;
30 import android.graphics.Path;
31 import android.graphics.Rect;
32 import android.graphics.RectF;
33 import android.graphics.drawable.AdaptiveIconDrawable;
34 import android.graphics.drawable.ColorDrawable;
35 import android.graphics.drawable.Drawable;
36 import android.os.Build;
37 import android.util.AttributeSet;
38 import android.view.View;
39 import android.view.ViewOutlineProvider;
40 
41 import androidx.annotation.Nullable;
42 import androidx.dynamicanimation.animation.FloatPropertyCompat;
43 import androidx.dynamicanimation.animation.SpringAnimation;
44 import androidx.dynamicanimation.animation.SpringForce;
45 
46 import com.android.launcher3.DeviceProfile;
47 import com.android.launcher3.InsettableFrameLayout.LayoutParams;
48 import com.android.launcher3.Launcher;
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.IconShape;
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 @TargetApi(Build.VERSION_CODES.Q)
60 public class ClipIconView extends View implements ClipPathView {
61 
62     private static final Rect sTmpRect = new Rect();
63 
64     // We spring the foreground drawable relative to the icon's movement in the DragLayer.
65     // We then use these two factor values to scale the movement of the fg within this view.
66     private static final int FG_TRANS_X_FACTOR = 60;
67     private static final int FG_TRANS_Y_FACTOR = 75;
68 
69     private static final FloatPropertyCompat<ClipIconView> mFgTransYProperty =
70             new FloatPropertyCompat<ClipIconView>("ClipIconViewFgTransY") {
71                 @Override
72                 public float getValue(ClipIconView view) {
73                     return view.mFgTransY;
74                 }
75 
76                 @Override
77                 public void setValue(ClipIconView view, float transY) {
78                     view.mFgTransY = transY;
79                     view.invalidate();
80                 }
81             };
82 
83     private static final FloatPropertyCompat<ClipIconView> mFgTransXProperty =
84             new FloatPropertyCompat<ClipIconView>("ClipIconViewFgTransX") {
85                 @Override
86                 public float getValue(ClipIconView view) {
87                     return view.mFgTransX;
88                 }
89 
90                 @Override
91                 public void setValue(ClipIconView view, float transX) {
92                     view.mFgTransX = transX;
93                     view.invalidate();
94                 }
95             };
96 
97     private final Launcher mLauncher;
98     private final int mBlurSizeOutline;
99     private final boolean mIsRtl;
100 
101     private @Nullable Drawable mForeground;
102     private @Nullable Drawable mBackground;
103 
104     private boolean mIsAdaptiveIcon = false;
105 
106     private ValueAnimator mRevealAnimator;
107 
108     private final Rect mStartRevealRect = new Rect();
109     private final Rect mEndRevealRect = new Rect();
110     private Path mClipPath;
111     private float mTaskCornerRadius;
112 
113     private final Rect mOutline = new Rect();
114     private final Rect mFinalDrawableBounds = new Rect();
115 
116     private final SpringAnimation mFgSpringY;
117     private float mFgTransY;
118     private final SpringAnimation mFgSpringX;
119     private float mFgTransX;
120 
ClipIconView(Context context)121     public ClipIconView(Context context) {
122         this(context, null);
123     }
124 
ClipIconView(Context context, AttributeSet attrs)125     public ClipIconView(Context context, AttributeSet attrs) {
126         this(context, attrs, 0);
127     }
128 
ClipIconView(Context context, AttributeSet attrs, int defStyleAttr)129     public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
130         super(context, attrs, defStyleAttr);
131         mLauncher = Launcher.getLauncher(context);
132         mBlurSizeOutline = getResources().getDimensionPixelSize(
133                 R.dimen.blur_size_medium_outline);
134         mIsRtl = Utilities.isRtl(getResources());
135 
136         mFgSpringX = new SpringAnimation(this, mFgTransXProperty)
137                 .setSpring(new SpringForce()
138                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
139                         .setStiffness(SpringForce.STIFFNESS_LOW));
140         mFgSpringY = new SpringAnimation(this, mFgTransYProperty)
141                 .setSpring(new SpringForce()
142                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
143                         .setStiffness(SpringForce.STIFFNESS_LOW));
144     }
145 
update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, float scale, float minSize, LayoutParams parentLp, boolean isVerticalBarLayout)146     void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
147             boolean isOpening, float scale, float minSize, LayoutParams parentLp,
148             boolean isVerticalBarLayout) {
149         DeviceProfile dp = mLauncher.getDeviceProfile();
150         float dX = mIsRtl
151                 ? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width)
152                 : rect.left - parentLp.getMarginStart();
153         float dY = rect.top - parentLp.topMargin;
154 
155         // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
156         float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
157         float shapeRevealProgress = Utilities.boundToRange(mapToRange(
158                 Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
159                 LINEAR), 0, 1);
160 
161         if (isVerticalBarLayout) {
162             mOutline.right = (int) (rect.width() / scale);
163         } else {
164             mOutline.bottom = (int) (rect.height() / scale);
165         }
166 
167         mTaskCornerRadius = cornerRadius / scale;
168         if (mIsAdaptiveIcon) {
169             if (!isOpening && progress >= shapeProgressStart) {
170                 if (mRevealAnimator == null) {
171                     mRevealAnimator = (ValueAnimator) IconShape.getShape().createRevealAnimator(
172                             this, mStartRevealRect, mOutline, mTaskCornerRadius, !isOpening);
173                     mRevealAnimator.addListener(new AnimatorListenerAdapter() {
174                         @Override
175                         public void onAnimationEnd(Animator animation) {
176                             mRevealAnimator = null;
177                         }
178                     });
179                     mRevealAnimator.start();
180                     // We pause here so we can set the current fraction ourselves.
181                     mRevealAnimator.pause();
182                 }
183                 mRevealAnimator.setCurrentFraction(shapeRevealProgress);
184             }
185 
186             float drawableScale = (isVerticalBarLayout ? mOutline.width() : mOutline.height())
187                     / minSize;
188             setBackgroundDrawableBounds(drawableScale, isVerticalBarLayout);
189             if (isOpening) {
190                 // Center align foreground
191                 int height = mFinalDrawableBounds.height();
192                 int width = mFinalDrawableBounds.width();
193                 int diffY = isVerticalBarLayout ? 0
194                         : (int) (((height * drawableScale) - height) / 2);
195                 int diffX = isVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2)
196                         : 0;
197                 sTmpRect.set(mFinalDrawableBounds);
198                 sTmpRect.offset(diffX, diffY);
199                 mForeground.setBounds(sTmpRect);
200             } else {
201                 // Spring the foreground relative to the icon's movement within the DragLayer.
202                 int diffX = (int) (dX / dp.availableWidthPx * FG_TRANS_X_FACTOR);
203                 int diffY = (int) (dY / dp.availableHeightPx * FG_TRANS_Y_FACTOR);
204 
205                 mFgSpringX.animateToFinalPosition(diffX);
206                 mFgSpringY.animateToFinalPosition(diffY);
207             }
208         }
209         invalidate();
210         invalidateOutline();
211     }
212 
setBackgroundDrawableBounds(float scale, boolean isVerticalBarLayout)213     private void setBackgroundDrawableBounds(float scale, boolean isVerticalBarLayout) {
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 (isVerticalBarLayout) {
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 
setIcon(@ullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening, boolean isVerticalBarLayout)231     void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening,
232             boolean isVerticalBarLayout) {
233         mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
234         if (mIsAdaptiveIcon) {
235             boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
236 
237             AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
238             Drawable background = adaptiveIcon.getBackground();
239             if (background == null) {
240                 background = new ColorDrawable(Color.TRANSPARENT);
241             }
242             mBackground = background;
243             Drawable foreground = adaptiveIcon.getForeground();
244             if (foreground == null) {
245                 foreground = new ColorDrawable(Color.TRANSPARENT);
246             }
247             mForeground = foreground;
248 
249             final int originalHeight = lp.height;
250             final int originalWidth = lp.width;
251 
252             int blurMargin = mBlurSizeOutline / 2;
253             mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
254 
255             if (!isFolderIcon) {
256                 mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
257             }
258             mForeground.setBounds(mFinalDrawableBounds);
259             mBackground.setBounds(mFinalDrawableBounds);
260 
261             mStartRevealRect.set(0, 0, originalWidth, originalHeight);
262 
263             if (!isFolderIcon) {
264                 Utilities.scaleRectAboutCenter(mStartRevealRect, IconShape.getNormalizationScale());
265             }
266 
267             float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
268             if (isVerticalBarLayout) {
269                 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
270             } else {
271                 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
272             }
273 
274             int left = mIsRtl
275                     ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
276                     : lp.leftMargin;
277             layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
278 
279             float scale = Math.max((float) lp.height / originalHeight,
280                     (float) lp.width / originalWidth);
281             float bgDrawableStartScale;
282             if (isOpening) {
283                 bgDrawableStartScale = 1f;
284                 mOutline.set(0, 0, originalWidth, originalHeight);
285             } else {
286                 bgDrawableStartScale = scale;
287                 mOutline.set(0, 0, lp.width, lp.height);
288             }
289             setBackgroundDrawableBounds(bgDrawableStartScale, isVerticalBarLayout);
290             mEndRevealRect.set(0, 0, lp.width, lp.height);
291             setOutlineProvider(new ViewOutlineProvider() {
292                 @Override
293                 public void getOutline(View view, Outline outline) {
294                     outline.setRoundRect(mOutline, mTaskCornerRadius);
295                 }
296             });
297             setClipToOutline(true);
298         } else {
299             setBackground(drawable);
300             setClipToOutline(false);
301         }
302 
303         invalidate();
304         invalidateOutline();
305     }
306 
307     @Override
setClipPath(Path clipPath)308     public void setClipPath(Path clipPath) {
309         mClipPath = clipPath;
310         invalidate();
311     }
312 
313     @Override
draw(Canvas canvas)314     public void draw(Canvas canvas) {
315         int count = canvas.save();
316         if (mClipPath != null) {
317             canvas.clipPath(mClipPath);
318         }
319         super.draw(canvas);
320         if (mBackground != null) {
321             mBackground.draw(canvas);
322         }
323         if (mForeground != null) {
324             int count2 = canvas.save();
325             canvas.translate(mFgTransX, mFgTransY);
326             mForeground.draw(canvas);
327             canvas.restoreToCount(count2);
328         }
329         canvas.restoreToCount(count);
330     }
331 
recycle()332     void recycle() {
333         setBackground(null);
334         mIsAdaptiveIcon = false;
335         mForeground = null;
336         mBackground = null;
337         mClipPath = null;
338         mFinalDrawableBounds.setEmpty();
339         if (mRevealAnimator != null) {
340             mRevealAnimator.cancel();
341         }
342         mRevealAnimator = null;
343         mTaskCornerRadius = 0;
344         mOutline.setEmpty();
345         mFgTransY = 0;
346         mFgSpringX.cancel();
347         mFgTransX = 0;
348         mFgSpringY.cancel();
349     }
350 }
351