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