1 /* 2 * Copyright (C) 2023 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 17 package com.android.wm.shell.desktopmode; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 22 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 23 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.RectEvaluator; 28 import android.animation.ValueAnimator; 29 import android.annotation.NonNull; 30 import android.app.ActivityManager; 31 import android.app.WindowConfiguration; 32 import android.content.Context; 33 import android.content.res.Resources; 34 import android.graphics.PixelFormat; 35 import android.graphics.PointF; 36 import android.graphics.Rect; 37 import android.graphics.Region; 38 import android.graphics.drawable.LayerDrawable; 39 import android.util.DisplayMetrics; 40 import android.view.SurfaceControl; 41 import android.view.SurfaceControlViewHost; 42 import android.view.View; 43 import android.view.WindowManager; 44 import android.view.WindowlessWindowManager; 45 import android.view.animation.DecelerateInterpolator; 46 47 import androidx.annotation.VisibleForTesting; 48 49 import com.android.wm.shell.R; 50 import com.android.wm.shell.RootTaskDisplayAreaOrganizer; 51 import com.android.wm.shell.common.DisplayController; 52 import com.android.wm.shell.common.DisplayLayout; 53 import com.android.wm.shell.common.SyncTransactionQueue; 54 55 /** 56 * Animated visual indicator for Desktop Mode windowing transitions. 57 */ 58 public class DesktopModeVisualIndicator { 59 public enum IndicatorType { 60 /** To be used when we don't want to indicate any transition */ 61 NO_INDICATOR, 62 /** Indicates impending transition into desktop mode */ 63 TO_DESKTOP_INDICATOR, 64 /** Indicates impending transition into fullscreen */ 65 TO_FULLSCREEN_INDICATOR, 66 /** Indicates impending transition into split select on the left side */ 67 TO_SPLIT_LEFT_INDICATOR, 68 /** Indicates impending transition into split select on the right side */ 69 TO_SPLIT_RIGHT_INDICATOR 70 } 71 72 private final Context mContext; 73 private final DisplayController mDisplayController; 74 private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; 75 private final ActivityManager.RunningTaskInfo mTaskInfo; 76 private final SurfaceControl mTaskSurface; 77 private SurfaceControl mLeash; 78 79 private final SyncTransactionQueue mSyncQueue; 80 private SurfaceControlViewHost mViewHost; 81 82 private View mView; 83 private IndicatorType mCurrentType; 84 DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer)85 public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, 86 ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, 87 Context context, SurfaceControl taskSurface, 88 RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer) { 89 mSyncQueue = syncQueue; 90 mTaskInfo = taskInfo; 91 mDisplayController = displayController; 92 mContext = context; 93 mTaskSurface = taskSurface; 94 mRootTdaOrganizer = taskDisplayAreaOrganizer; 95 mCurrentType = IndicatorType.NO_INDICATOR; 96 } 97 98 /** 99 * Based on the coordinates of the current drag event, determine which indicator type we should 100 * display, including no visible indicator. 101 */ 102 @NonNull updateIndicatorType(PointF inputCoordinates, int windowingMode)103 IndicatorType updateIndicatorType(PointF inputCoordinates, int windowingMode) { 104 final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); 105 // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. 106 IndicatorType result = IndicatorType.NO_INDICATOR; 107 final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( 108 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); 109 // Because drags in freeform use task position for indicator calculation, we need to 110 // account for the possibility of the task going off the top of the screen by captionHeight 111 final int captionHeight = mContext.getResources().getDimensionPixelSize( 112 com.android.wm.shell.R.dimen.desktop_mode_freeform_decor_caption_height); 113 final Region fullscreenRegion = calculateFullscreenRegion(layout, windowingMode, 114 captionHeight); 115 final Region splitLeftRegion = calculateSplitLeftRegion(layout, windowingMode, 116 transitionAreaWidth, captionHeight); 117 final Region splitRightRegion = calculateSplitRightRegion(layout, windowingMode, 118 transitionAreaWidth, captionHeight); 119 final Region toDesktopRegion = calculateToDesktopRegion(layout, windowingMode, 120 splitLeftRegion, splitRightRegion, fullscreenRegion); 121 if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { 122 result = IndicatorType.TO_FULLSCREEN_INDICATOR; 123 } 124 if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { 125 result = IndicatorType.TO_SPLIT_LEFT_INDICATOR; 126 } 127 if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { 128 result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR; 129 } 130 if (toDesktopRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { 131 result = IndicatorType.TO_DESKTOP_INDICATOR; 132 } 133 transitionIndicator(result); 134 return result; 135 } 136 137 @VisibleForTesting calculateFullscreenRegion(DisplayLayout layout, @WindowConfiguration.WindowingMode int windowingMode, int captionHeight)138 Region calculateFullscreenRegion(DisplayLayout layout, 139 @WindowConfiguration.WindowingMode int windowingMode, int captionHeight) { 140 final Region region = new Region(); 141 int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM 142 ? mContext.getResources().getDimensionPixelSize( 143 com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height) 144 : 2 * layout.stableInsets().top; 145 // A thin, short Rect at the top of the screen. 146 if (windowingMode == WINDOWING_MODE_FREEFORM) { 147 int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( 148 com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width); 149 region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2), 150 -captionHeight, 151 (layout.width() / 2) + (fromFreeformWidth / 2), 152 transitionHeight)); 153 } 154 // A screen-wide, shorter Rect if the task is in fullscreen or split. 155 if (windowingMode == WINDOWING_MODE_FULLSCREEN 156 || windowingMode == WINDOWING_MODE_MULTI_WINDOW) { 157 region.union(new Rect(0, 158 -captionHeight, 159 layout.width(), 160 transitionHeight)); 161 } 162 return region; 163 } 164 165 @VisibleForTesting calculateToDesktopRegion(DisplayLayout layout, @WindowConfiguration.WindowingMode int windowingMode, Region splitLeftRegion, Region splitRightRegion, Region toFullscreenRegion)166 Region calculateToDesktopRegion(DisplayLayout layout, 167 @WindowConfiguration.WindowingMode int windowingMode, 168 Region splitLeftRegion, Region splitRightRegion, 169 Region toFullscreenRegion) { 170 final Region region = new Region(); 171 // If in desktop, we need no region. Otherwise it's the same for all windowing modes. 172 if (windowingMode != WINDOWING_MODE_FREEFORM) { 173 region.union(new Rect(0, 0, layout.width(), layout.height())); 174 region.op(splitLeftRegion, Region.Op.DIFFERENCE); 175 region.op(splitRightRegion, Region.Op.DIFFERENCE); 176 region.op(toFullscreenRegion, Region.Op.DIFFERENCE); 177 } 178 return region; 179 } 180 181 @VisibleForTesting calculateSplitLeftRegion(DisplayLayout layout, @WindowConfiguration.WindowingMode int windowingMode, int transitionEdgeWidth, int captionHeight)182 Region calculateSplitLeftRegion(DisplayLayout layout, 183 @WindowConfiguration.WindowingMode int windowingMode, 184 int transitionEdgeWidth, int captionHeight) { 185 final Region region = new Region(); 186 // In freeform, keep the top corners clear. 187 int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM 188 ? mContext.getResources().getDimensionPixelSize( 189 com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : 190 -captionHeight; 191 region.union(new Rect(0, transitionHeight, transitionEdgeWidth, layout.height())); 192 return region; 193 } 194 195 @VisibleForTesting calculateSplitRightRegion(DisplayLayout layout, @WindowConfiguration.WindowingMode int windowingMode, int transitionEdgeWidth, int captionHeight)196 Region calculateSplitRightRegion(DisplayLayout layout, 197 @WindowConfiguration.WindowingMode int windowingMode, 198 int transitionEdgeWidth, int captionHeight) { 199 final Region region = new Region(); 200 // In freeform, keep the top corners clear. 201 int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM 202 ? mContext.getResources().getDimensionPixelSize( 203 com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : 204 -captionHeight; 205 region.union(new Rect(layout.width() - transitionEdgeWidth, transitionHeight, 206 layout.width(), layout.height())); 207 return region; 208 } 209 210 /** 211 * Create a fullscreen indicator with no animation 212 */ createView()213 private void createView() { 214 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 215 final Resources resources = mContext.getResources(); 216 final DisplayMetrics metrics = resources.getDisplayMetrics(); 217 final int screenWidth = metrics.widthPixels; 218 final int screenHeight = metrics.heightPixels; 219 220 mView = new View(mContext); 221 final SurfaceControl.Builder builder = new SurfaceControl.Builder(); 222 mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); 223 mLeash = builder 224 .setName("Desktop Mode Visual Indicator") 225 .setContainerLayer() 226 .setCallsite("DesktopModeVisualIndicator.createView") 227 .build(); 228 t.show(mLeash); 229 final WindowManager.LayoutParams lp = 230 new WindowManager.LayoutParams(screenWidth, screenHeight, TYPE_APPLICATION, 231 FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); 232 lp.setTitle("Desktop Mode Visual Indicator"); 233 lp.setTrustedOverlay(); 234 final WindowlessWindowManager windowManager = new WindowlessWindowManager( 235 mTaskInfo.configuration, mLeash, 236 null /* hostInputToken */); 237 mViewHost = new SurfaceControlViewHost(mContext, 238 mDisplayController.getDisplay(mTaskInfo.displayId), windowManager, 239 "DesktopModeVisualIndicator"); 240 mViewHost.setView(mView, lp); 241 // We want this indicator to be behind the dragged task, but in front of all others. 242 t.setRelativeLayer(mLeash, mTaskSurface, -1); 243 244 mSyncQueue.runInSync(transaction -> { 245 transaction.merge(t); 246 t.close(); 247 }); 248 } 249 250 /** 251 * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. 252 */ fadeInIndicator(IndicatorType type)253 private void fadeInIndicator(IndicatorType type) { 254 mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background); 255 final VisualIndicatorAnimator animator = VisualIndicatorAnimator 256 .fadeBoundsIn(mView, type, 257 mDisplayController.getDisplayLayout(mTaskInfo.displayId)); 258 animator.start(); 259 mCurrentType = type; 260 } 261 262 /** 263 * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. 264 */ fadeOutIndicator()265 private void fadeOutIndicator() { 266 final VisualIndicatorAnimator animator = VisualIndicatorAnimator 267 .fadeBoundsOut(mView, mCurrentType, 268 mDisplayController.getDisplayLayout(mTaskInfo.displayId)); 269 animator.start(); 270 mCurrentType = IndicatorType.NO_INDICATOR; 271 } 272 273 /** 274 * Takes existing indicator and animates it to bounds reflecting a new indicator type. 275 */ transitionIndicator(IndicatorType newType)276 private void transitionIndicator(IndicatorType newType) { 277 if (mCurrentType == newType) return; 278 if (mView == null) { 279 createView(); 280 } 281 if (mCurrentType == IndicatorType.NO_INDICATOR) { 282 fadeInIndicator(newType); 283 } else if (newType == IndicatorType.NO_INDICATOR) { 284 fadeOutIndicator(); 285 } else { 286 final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( 287 mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, 288 newType); 289 mCurrentType = newType; 290 animator.start(); 291 } 292 } 293 294 /** 295 * Release the indicator and its components when it is no longer needed. 296 */ releaseVisualIndicator(SurfaceControl.Transaction t)297 public void releaseVisualIndicator(SurfaceControl.Transaction t) { 298 if (mViewHost == null) return; 299 if (mViewHost != null) { 300 mViewHost.release(); 301 mViewHost = null; 302 } 303 304 if (mLeash != null) { 305 t.remove(mLeash); 306 mLeash = null; 307 } 308 } 309 310 /** 311 * Animator for Desktop Mode transitions which supports bounds and alpha animation. 312 */ 313 private static class VisualIndicatorAnimator extends ValueAnimator { 314 private static final int FULLSCREEN_INDICATOR_DURATION = 200; 315 private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f; 316 private static final float INDICATOR_FINAL_OPACITY = 0.35f; 317 private static final int MAXIMUM_OPACITY = 255; 318 319 /** 320 * Determines how this animator will interact with the view's alpha: 321 * Fade in, fade out, or no change to alpha 322 */ 323 private enum AlphaAnimType { 324 ALPHA_FADE_IN_ANIM, ALPHA_FADE_OUT_ANIM, ALPHA_NO_CHANGE_ANIM 325 } 326 327 private final View mView; 328 private final Rect mStartBounds; 329 private final Rect mEndBounds; 330 private final RectEvaluator mRectEvaluator; 331 VisualIndicatorAnimator(View view, Rect startBounds, Rect endBounds)332 private VisualIndicatorAnimator(View view, Rect startBounds, 333 Rect endBounds) { 334 mView = view; 335 mStartBounds = new Rect(startBounds); 336 mEndBounds = endBounds; 337 setFloatValues(0, 1); 338 mRectEvaluator = new RectEvaluator(new Rect()); 339 } 340 fadeBoundsIn( @onNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout)341 private static VisualIndicatorAnimator fadeBoundsIn( 342 @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) { 343 final Rect startBounds = getIndicatorBounds(displayLayout, type); 344 view.getBackground().setBounds(startBounds); 345 346 final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( 347 view, startBounds, getMaxBounds(startBounds)); 348 animator.setInterpolator(new DecelerateInterpolator()); 349 setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM); 350 return animator; 351 } 352 fadeBoundsOut( @onNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout)353 private static VisualIndicatorAnimator fadeBoundsOut( 354 @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) { 355 final Rect endBounds = getIndicatorBounds(displayLayout, type); 356 final Rect startBounds = getMaxBounds(endBounds); 357 view.getBackground().setBounds(startBounds); 358 359 final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( 360 view, startBounds, endBounds); 361 animator.setInterpolator(new DecelerateInterpolator()); 362 setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_OUT_ANIM); 363 return animator; 364 } 365 366 /** 367 * Create animator for visual indicator changing type (i.e., fullscreen to freeform, 368 * freeform to split, etc.) 369 * 370 * @param view the view for this indicator 371 * @param displayLayout information about the display the transitioning task is currently on 372 * @param origType the original indicator type 373 * @param newType the new indicator type 374 */ animateIndicatorType(@onNull View view, @NonNull DisplayLayout displayLayout, IndicatorType origType, IndicatorType newType)375 private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view, 376 @NonNull DisplayLayout displayLayout, IndicatorType origType, 377 IndicatorType newType) { 378 final Rect startBounds = getIndicatorBounds(displayLayout, origType); 379 final Rect endBounds = getIndicatorBounds(displayLayout, newType); 380 final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( 381 view, startBounds, endBounds); 382 animator.setInterpolator(new DecelerateInterpolator()); 383 setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_NO_CHANGE_ANIM); 384 return animator; 385 } 386 getIndicatorBounds(DisplayLayout layout, IndicatorType type)387 private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type) { 388 final int padding = layout.stableInsets().top; 389 switch (type) { 390 case TO_FULLSCREEN_INDICATOR: 391 return new Rect(padding, padding, 392 layout.width() - padding, 393 layout.height() - padding); 394 case TO_DESKTOP_INDICATOR: 395 final float adjustmentPercentage = 1f 396 - DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; 397 return new Rect((int) (adjustmentPercentage * layout.width() / 2), 398 (int) (adjustmentPercentage * layout.height() / 2), 399 (int) (layout.width() - (adjustmentPercentage * layout.width() / 2)), 400 (int) (layout.height() - (adjustmentPercentage * layout.height() / 2))); 401 case TO_SPLIT_LEFT_INDICATOR: 402 return new Rect(padding, padding, 403 layout.width() / 2 - padding, 404 layout.height() - padding); 405 case TO_SPLIT_RIGHT_INDICATOR: 406 return new Rect(layout.width() / 2 + padding, padding, 407 layout.width() - padding, 408 layout.height() - padding); 409 default: 410 throw new IllegalArgumentException("Invalid indicator type provided."); 411 } 412 } 413 414 /** 415 * Add necessary listener for animation of indicator 416 */ setupIndicatorAnimation(@onNull VisualIndicatorAnimator animator, AlphaAnimType animType)417 private static void setupIndicatorAnimation(@NonNull VisualIndicatorAnimator animator, 418 AlphaAnimType animType) { 419 animator.addUpdateListener(a -> { 420 if (animator.mView != null) { 421 animator.updateBounds(a.getAnimatedFraction(), animator.mView); 422 if (animType == AlphaAnimType.ALPHA_FADE_IN_ANIM) { 423 animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView); 424 } else if (animType == AlphaAnimType.ALPHA_FADE_OUT_ANIM) { 425 animator.updateIndicatorAlpha(1 - a.getAnimatedFraction(), animator.mView); 426 } 427 } else { 428 animator.cancel(); 429 } 430 }); 431 animator.addListener(new AnimatorListenerAdapter() { 432 @Override 433 public void onAnimationEnd(Animator animation) { 434 animator.mView.getBackground().setBounds(animator.mEndBounds); 435 } 436 }); 437 animator.setDuration(FULLSCREEN_INDICATOR_DURATION); 438 } 439 440 /** 441 * Update bounds of view based on current animation fraction. 442 * Use of delta is to animate bounds independently, in case we need to 443 * run multiple animations simultaneously. 444 * 445 * @param fraction fraction to use, compared against previous fraction 446 * @param view the view to update 447 */ updateBounds(float fraction, View view)448 private void updateBounds(float fraction, View view) { 449 if (mStartBounds.equals(mEndBounds)) { 450 return; 451 } 452 final Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds); 453 view.getBackground().setBounds(currentBounds); 454 } 455 456 /** 457 * Fade in the fullscreen indicator 458 * 459 * @param fraction current animation fraction 460 */ updateIndicatorAlpha(float fraction, View view)461 private void updateIndicatorAlpha(float fraction, View view) { 462 final LayerDrawable drawable = (LayerDrawable) view.getBackground(); 463 drawable.findDrawableByLayerId(R.id.indicator_stroke) 464 .setAlpha((int) (MAXIMUM_OPACITY * fraction)); 465 drawable.findDrawableByLayerId(R.id.indicator_solid) 466 .setAlpha((int) (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY)); 467 } 468 469 /** 470 * Return the max bounds of a visual indicator 471 */ getMaxBounds(Rect startBounds)472 private static Rect getMaxBounds(Rect startBounds) { 473 return new Rect((int) (startBounds.left 474 - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())), 475 (int) (startBounds.top 476 - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())), 477 (int) (startBounds.right 478 + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())), 479 (int) (startBounds.bottom 480 + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height()))); 481 } 482 } 483 } 484