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