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