1 /* 2 * Copyright (C) 2021 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.quickstep.views; 17 18 import android.animation.Animator; 19 import android.animation.Animator.AnimatorListener; 20 import android.annotation.TargetApi; 21 import android.content.Context; 22 import android.graphics.Matrix; 23 import android.graphics.RectF; 24 import android.os.Build; 25 import android.util.AttributeSet; 26 import android.util.Size; 27 import android.view.GhostView; 28 import android.view.RemoteAnimationTarget; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 32 import android.widget.FrameLayout; 33 34 import androidx.annotation.Nullable; 35 36 import com.android.launcher3.R; 37 import com.android.launcher3.Utilities; 38 import com.android.launcher3.dragndrop.DragLayer; 39 import com.android.launcher3.uioverrides.QuickstepLauncher; 40 import com.android.launcher3.util.Themes; 41 import com.android.launcher3.views.FloatingView; 42 import com.android.launcher3.views.ListenerView; 43 import com.android.launcher3.widget.LauncherAppWidgetHostView; 44 import com.android.launcher3.widget.RoundedCornerEnforcement; 45 46 /** A view that mimics an App Widget through a launch animation. */ 47 @TargetApi(Build.VERSION_CODES.S) 48 public class FloatingWidgetView extends FrameLayout implements AnimatorListener, 49 OnGlobalLayoutListener, FloatingView { 50 private static final Matrix sTmpMatrix = new Matrix(); 51 52 private final QuickstepLauncher mLauncher; 53 private final ListenerView mListenerView; 54 private final FloatingWidgetBackgroundView mBackgroundView; 55 private final RectF mBackgroundOffset = new RectF(); 56 57 private LauncherAppWidgetHostView mAppWidgetView; 58 private View mAppWidgetBackgroundView; 59 private RectF mBackgroundPosition; 60 @Nullable 61 private GhostView mForegroundOverlayView; 62 63 @Nullable 64 private Runnable mEndRunnable; 65 @Nullable 66 private Runnable mFastFinishRunnable; 67 @Nullable 68 private Runnable mOnTargetChangeRunnable; 69 private boolean mAppTargetIsTranslucent; 70 71 private float mIconOffsetY; 72 FloatingWidgetView(Context context)73 public FloatingWidgetView(Context context) { 74 this(context, null); 75 } 76 FloatingWidgetView(Context context, @Nullable AttributeSet attrs)77 public FloatingWidgetView(Context context, @Nullable AttributeSet attrs) { 78 this(context, attrs, 0); 79 } 80 FloatingWidgetView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)81 public FloatingWidgetView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 82 super(context, attrs, defStyleAttr); 83 mLauncher = QuickstepLauncher.getLauncher(context); 84 mListenerView = new ListenerView(context, attrs); 85 mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr); 86 addView(mBackgroundView); 87 setWillNotDraw(false); 88 } 89 90 @Override onAnimationEnd(Animator animator)91 public void onAnimationEnd(Animator animator) { 92 Runnable endRunnable = mEndRunnable; 93 mEndRunnable = null; 94 if (endRunnable != null) { 95 endRunnable.run(); 96 } 97 } 98 99 @Override onAnimationStart(Animator animator)100 public void onAnimationStart(Animator animator) { 101 } 102 103 @Override onAnimationCancel(Animator animator)104 public void onAnimationCancel(Animator animator) { 105 } 106 107 @Override onAnimationRepeat(Animator animator)108 public void onAnimationRepeat(Animator animator) { 109 } 110 111 @Override onAttachedToWindow()112 protected void onAttachedToWindow() { 113 super.onAttachedToWindow(); 114 getViewTreeObserver().addOnGlobalLayoutListener(this); 115 } 116 117 @Override onDetachedFromWindow()118 protected void onDetachedFromWindow() { 119 getViewTreeObserver().removeOnGlobalLayoutListener(this); 120 super.onDetachedFromWindow(); 121 } 122 123 @Override onGlobalLayout()124 public void onGlobalLayout() { 125 if (isUninitialized()) return; 126 positionViews(); 127 if (mOnTargetChangeRunnable != null) { 128 mOnTargetChangeRunnable.run(); 129 } 130 } 131 132 /** Sets a runnable that is called on global layout change. */ setOnTargetChangeListener(Runnable onTargetChangeListener)133 public void setOnTargetChangeListener(Runnable onTargetChangeListener) { 134 mOnTargetChangeRunnable = onTargetChangeListener; 135 } 136 137 /** Sets a runnable that is called after a call to {@link #fastFinish()}. */ setFastFinishRunnable(Runnable runnable)138 public void setFastFinishRunnable(Runnable runnable) { 139 mFastFinishRunnable = runnable; 140 } 141 142 /** Callback at the end or early exit of the animation. */ 143 @Override fastFinish()144 public void fastFinish() { 145 if (isUninitialized()) return; 146 Runnable fastFinishRunnable = mFastFinishRunnable; 147 if (fastFinishRunnable != null) { 148 fastFinishRunnable.run(); 149 } 150 Runnable endRunnable = mEndRunnable; 151 mEndRunnable = null; 152 if (endRunnable != null) { 153 endRunnable.run(); 154 } 155 } 156 init(DragLayer dragLayer, LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius, boolean appTargetIsTranslucent, int fallbackBackgroundColor)157 private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView, 158 RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius, 159 boolean appTargetIsTranslucent, int fallbackBackgroundColor) { 160 mAppWidgetView = originalView; 161 // Deferrals must begin before GhostView is created. See b/190818220 162 mAppWidgetView.beginDeferringUpdates(); 163 mBackgroundPosition = widgetBackgroundPosition; 164 mAppTargetIsTranslucent = appTargetIsTranslucent; 165 mEndRunnable = () -> finish(dragLayer); 166 167 mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView); 168 if (mAppWidgetBackgroundView == null) { 169 mAppWidgetBackgroundView = mAppWidgetView; 170 } 171 172 getRelativePosition(mAppWidgetBackgroundView, dragLayer, mBackgroundPosition); 173 getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset); 174 if (!mAppTargetIsTranslucent) { 175 mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius, 176 fallbackBackgroundColor); 177 // Layout call before GhostView creation so that the overlaid view isn't clipped 178 layout(0, 0, windowSize.getWidth(), windowSize.getHeight()); 179 mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this); 180 positionViews(); 181 } 182 183 mListenerView.setListener(this::fastFinish); 184 dragLayer.addView(mListenerView); 185 } 186 187 /** 188 * Updates the position and opacity of the floating widget's components. 189 * 190 * @param backgroundPosition the new position of the widget's background relative to the 191 * {@link FloatingWidgetView}'s parent 192 * @param floatingWidgetAlpha the overall opacity of the {@link FloatingWidgetView} 193 * @param foregroundAlpha the opacity of the foreground layer 194 * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App 195 * Widget doesn't have a background 196 * @param cornerRadiusProgress progress of the corner radius animation, where 0 is the 197 * original radius and 1 is the window radius 198 */ update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha, float fallbackBackgroundAlpha, float cornerRadiusProgress)199 public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha, 200 float fallbackBackgroundAlpha, float cornerRadiusProgress) { 201 if (isUninitialized() || mAppTargetIsTranslucent) return; 202 setAlpha(floatingWidgetAlpha); 203 mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha); 204 mAppWidgetView.setAlpha(foregroundAlpha); 205 mBackgroundPosition = backgroundPosition; 206 positionViews(); 207 } 208 209 @Override setPositionOffsetY(float y)210 public void setPositionOffsetY(float y) { 211 mIconOffsetY = y; 212 onGlobalLayout(); 213 } 214 215 /** Sets the layout parameters of the floating view and its background view child. */ positionViews()216 private void positionViews() { 217 LayoutParams layoutParams = (LayoutParams) getLayoutParams(); 218 layoutParams.setMargins(0, 0, 0, 0); 219 setLayoutParams(layoutParams); 220 221 // FloatingWidgetView layout is forced LTR 222 mBackgroundView.setTranslationX(mBackgroundPosition.left); 223 mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY); 224 LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams(); 225 backgroundParams.leftMargin = 0; 226 backgroundParams.topMargin = 0; 227 backgroundParams.width = (int) mBackgroundPosition.width(); 228 backgroundParams.height = (int) mBackgroundPosition.height(); 229 mBackgroundView.setLayoutParams(backgroundParams); 230 231 if (mForegroundOverlayView != null) { 232 sTmpMatrix.reset(); 233 float foregroundScale = 234 mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth(); 235 sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(), 236 -mBackgroundOffset.top - mAppWidgetView.getTop()); 237 sTmpMatrix.postScale(foregroundScale, foregroundScale); 238 sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top 239 + mIconOffsetY); 240 mForegroundOverlayView.setMatrix(sTmpMatrix); 241 } 242 } 243 finish(DragLayer dragLayer)244 private void finish(DragLayer dragLayer) { 245 mAppWidgetView.setAlpha(1f); 246 GhostView.removeGhost(mAppWidgetView); 247 ((ViewGroup) dragLayer.getParent()).removeView(this); 248 dragLayer.removeView(mListenerView); 249 mBackgroundView.finish(); 250 // Removing GhostView must occur before ending deferrals. See b/190818220 251 mAppWidgetView.endDeferringUpdates(); 252 recycle(); 253 mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this); 254 } 255 getInitialCornerRadius()256 public float getInitialCornerRadius() { 257 return mBackgroundView.getMaximumRadius(); 258 } 259 isUninitialized()260 private boolean isUninitialized() { 261 return mForegroundOverlayView == null; 262 } 263 recycle()264 private void recycle() { 265 mIconOffsetY = 0; 266 mEndRunnable = null; 267 mFastFinishRunnable = null; 268 mOnTargetChangeRunnable = null; 269 mBackgroundPosition = null; 270 mListenerView.setListener(null); 271 mAppWidgetView = null; 272 mForegroundOverlayView = null; 273 mAppWidgetBackgroundView = null; 274 mBackgroundView.recycle(); 275 } 276 277 /** 278 * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of 279 * {@param originalView}. 280 * 281 * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's 282 * background bounds 283 * @param windowSize the size of the window when launched 284 * @param windowCornerRadius the corner radius of the window 285 */ getFloatingWidgetView(QuickstepLauncher launcher, LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius, boolean appTargetsAreTranslucent, int fallbackBackgroundColor)286 public static FloatingWidgetView getFloatingWidgetView(QuickstepLauncher launcher, 287 LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, 288 Size windowSize, float windowCornerRadius, boolean appTargetsAreTranslucent, 289 int fallbackBackgroundColor) { 290 final DragLayer dragLayer = launcher.getDragLayer(); 291 ViewGroup parent = (ViewGroup) dragLayer.getParent(); 292 FloatingWidgetView floatingView = 293 launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent); 294 floatingView.recycle(); 295 296 floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize, 297 windowCornerRadius, appTargetsAreTranslucent, fallbackBackgroundColor); 298 parent.addView(floatingView); 299 return floatingView; 300 } 301 302 /** 303 * Extract a background color from a target's task description, or fall back to the given 304 * context's theme background color. 305 */ getDefaultBackgroundColor( Context context, RemoteAnimationTarget target)306 public static int getDefaultBackgroundColor( 307 Context context, RemoteAnimationTarget target) { 308 return (target != null && target.taskInfo != null 309 && target.taskInfo.taskDescription != null) 310 ? target.taskInfo.taskDescription.getBackgroundColor() 311 : Themes.getColorBackground(context); 312 } 313 getRelativePosition(View descendant, View ancestor, RectF position)314 private static void getRelativePosition(View descendant, View ancestor, RectF position) { 315 float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()}; 316 Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points, 317 false /* includeRootScroll */, true /* ignoreTransform */); 318 position.set( 319 Math.min(points[0], points[2]), 320 Math.min(points[1], points[3]), 321 Math.max(points[0], points[2]), 322 Math.max(points[1], points[3])); 323 } 324 } 325