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