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 package com.android.quickstep.views; 17 18 import static com.android.app.animation.Interpolators.EMPHASIZED; 19 import static com.android.app.animation.Interpolators.LINEAR; 20 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 21 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 22 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorSet; 26 import android.animation.ObjectAnimator; 27 import android.animation.RectEvaluator; 28 import android.animation.ValueAnimator; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.graphics.Outline; 32 import android.graphics.Rect; 33 import android.graphics.drawable.Drawable; 34 import android.util.AttributeSet; 35 import android.view.View; 36 import android.view.ViewAnimationUtils; 37 import android.view.ViewOutlineProvider; 38 import android.widget.FrameLayout; 39 import android.widget.ImageView; 40 import android.widget.TextView; 41 42 import androidx.annotation.Nullable; 43 44 import com.android.launcher3.R; 45 import com.android.launcher3.Utilities; 46 import com.android.launcher3.util.MultiPropertyFactory; 47 import com.android.launcher3.util.MultiValueAlpha; 48 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 49 import com.android.quickstep.util.RecentsOrientedState; 50 51 /** 52 * An icon app menu view which can be used in place of an IconView in overview TaskViews. 53 */ 54 public class IconAppChipView extends FrameLayout implements TaskViewIcon { 55 56 private static final int MENU_BACKGROUND_REVEAL_DURATION = 417; 57 private static final int MENU_BACKGROUND_HIDE_DURATION = 333; 58 59 private static final int NUM_ALPHA_CHANNELS = 3; 60 private static final int INDEX_CONTENT_ALPHA = 0; 61 private static final int INDEX_COLOR_FILTER_ALPHA = 1; 62 private static final int INDEX_MODAL_ALPHA = 2; 63 64 private final MultiValueAlpha mMultiValueAlpha; 65 66 private View mMenuAnchorView; 67 private IconView mIconView; 68 // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name. 69 private TextView mIconTextCollapsedView; 70 private TextView mIconTextExpandedView; 71 private ImageView mIconArrowView; 72 private final Rect mBackgroundRelativeLtrLocation = new Rect(); 73 final RectEvaluator mBackgroundAnimationRectEvaluator = 74 new RectEvaluator(mBackgroundRelativeLtrLocation); 75 private final int mCollapsedMenuDefaultWidth; 76 private final int mExpandedMenuDefaultWidth; 77 private final int mCollapsedMenuDefaultHeight; 78 private final int mExpandedMenuDefaultHeight; 79 private final int mIconMenuMarginTopStart; 80 private final int mMenuToChipGap; 81 private final int mBackgroundMarginTopStart; 82 private final int mAppNameHorizontalMargin; 83 private final int mIconViewMarginStart; 84 private final int mAppIconSize; 85 private final int mArrowSize; 86 private final int mIconViewDrawableExpandedSize; 87 private final int mArrowMarginEnd; 88 private AnimatorSet mAnimator; 89 90 private int mMaxWidth = Integer.MAX_VALUE; 91 92 private static final int INDEX_SPLIT_TRANSLATION = 0; 93 private static final int INDEX_MENU_TRANSLATION = 1; 94 private static final int INDEX_COUNT_TRANSLATION = 2; 95 96 private final MultiPropertyFactory<View> mViewTranslationX; 97 private final MultiPropertyFactory<View> mViewTranslationY; 98 99 /** 100 * Gets the view split x-axis translation 101 */ getSplitTranslationX()102 public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() { 103 return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION); 104 } 105 106 /** 107 * Sets the view split x-axis translation 108 * @param translationX x-axis translation 109 */ setSplitTranslationX(float translationX)110 public void setSplitTranslationX(float translationX) { 111 getSplitTranslationX().setValue(translationX); 112 } 113 114 /** 115 * Gets the view split y-axis translation 116 */ getSplitTranslationY()117 public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() { 118 return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION); 119 } 120 121 /** 122 * Sets the view split y-axis translation 123 * @param translationY y-axis translation 124 */ setSplitTranslationY(float translationY)125 public void setSplitTranslationY(float translationY) { 126 getSplitTranslationY().setValue(translationY); 127 } 128 129 /** 130 * Gets the menu x-axis translation for split task 131 */ getMenuTranslationX()132 public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() { 133 return mViewTranslationX.get(INDEX_MENU_TRANSLATION); 134 } 135 136 /** 137 * Gets the menu y-axis translation for split task 138 */ getMenuTranslationY()139 public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() { 140 return mViewTranslationY.get(INDEX_MENU_TRANSLATION); 141 } 142 IconAppChipView(Context context)143 public IconAppChipView(Context context) { 144 this(context, null); 145 } 146 IconAppChipView(Context context, AttributeSet attrs)147 public IconAppChipView(Context context, AttributeSet attrs) { 148 this(context, attrs, 0); 149 } 150 IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr)151 public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) { 152 this(context, attrs, defStyleAttr, 0); 153 } 154 IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)155 public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 156 int defStyleRes) { 157 super(context, attrs, defStyleAttr, defStyleRes); 158 Resources res = getResources(); 159 mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS); 160 mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true); 161 162 // Menu dimensions 163 mCollapsedMenuDefaultWidth = 164 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width); 165 mExpandedMenuDefaultWidth = 166 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width); 167 mCollapsedMenuDefaultHeight = 168 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height); 169 mExpandedMenuDefaultHeight = 170 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height); 171 mIconMenuMarginTopStart = res.getDimensionPixelSize( 172 R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin); 173 mMenuToChipGap = res.getDimensionPixelSize( 174 R.dimen.task_thumbnail_icon_menu_expanded_gap); 175 176 // Background dimensions 177 mBackgroundMarginTopStart = res.getDimensionPixelSize( 178 R.dimen.task_thumbnail_icon_menu_background_margin_top_start); 179 180 // Contents dimensions 181 mAppNameHorizontalMargin = res.getDimensionPixelSize( 182 R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed); 183 mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin); 184 mIconViewMarginStart = res.getDimensionPixelSize( 185 R.dimen.task_thumbnail_icon_view_start_margin); 186 mAppIconSize = res.getDimensionPixelSize( 187 R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size); 188 mArrowSize = res.getDimensionPixelSize( 189 R.dimen.task_thumbnail_icon_menu_arrow_size); 190 mIconViewDrawableExpandedSize = res.getDimensionPixelSize( 191 R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size); 192 193 mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X, 194 INDEX_COUNT_TRANSLATION, 195 Float::sum); 196 mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y, 197 INDEX_COUNT_TRANSLATION, 198 Float::sum); 199 } 200 201 @Override onFinishInflate()202 protected void onFinishInflate() { 203 super.onFinishInflate(); 204 mIconView = findViewById(R.id.icon_view); 205 mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed); 206 mIconTextExpandedView = findViewById(R.id.icon_text_expanded); 207 mIconArrowView = findViewById(R.id.icon_arrow); 208 mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor); 209 } 210 getIconView()211 protected IconView getIconView() { 212 return mIconView; 213 } 214 215 @Override setText(CharSequence text)216 public void setText(CharSequence text) { 217 if (mIconTextCollapsedView != null) { 218 mIconTextCollapsedView.setText(text); 219 } 220 if (mIconTextExpandedView != null) { 221 mIconTextExpandedView.setText(text); 222 } 223 } 224 225 @Override getDrawable()226 public Drawable getDrawable() { 227 return mIconView == null ? null : mIconView.getDrawable(); 228 } 229 230 @Override setDrawable(Drawable icon)231 public void setDrawable(Drawable icon) { 232 if (mIconView != null) { 233 mIconView.setDrawable(icon); 234 } 235 } 236 237 @Override setDrawableSize(int iconWidth, int iconHeight)238 public void setDrawableSize(int iconWidth, int iconHeight) { 239 if (mIconView != null) { 240 mIconView.setDrawableSize(iconWidth, iconHeight); 241 } 242 } 243 244 /** 245 * Sets the maximum width of this Icon Menu. This is usually used when space is limited for 246 * split screen. 247 */ setMaxWidth(int maxWidth)248 public void setMaxWidth(int maxWidth) { 249 // Width showing only the app icon and arrow. Max width should not be set to less than this. 250 int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd; 251 mMaxWidth = Math.max(maxWidth, minimumMaxWidth); 252 } 253 254 @Override setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask)255 public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) { 256 RecentsPagedOrientationHandler orientationHandler = 257 orientationState.getOrientationHandler(); 258 // Layout params for anchor view 259 LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams(); 260 anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap; 261 mMenuAnchorView.setLayoutParams(anchorLayoutParams); 262 263 // Layout Params for the Menu View (this) 264 LayoutParams iconMenuParams = (LayoutParams) getLayoutParams(); 265 iconMenuParams.width = mExpandedMenuDefaultWidth; 266 iconMenuParams.height = mExpandedMenuDefaultHeight; 267 orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart, 268 mIconMenuMarginTopStart); 269 setLayoutParams(iconMenuParams); 270 271 // Layout params for the background 272 Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds(); 273 mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds); 274 setOutlineProvider(new ViewOutlineProvider() { 275 final Rect mRtlAppliedOutlineBounds = new Rect(); 276 @Override 277 public void getOutline(View view, Outline outline) { 278 mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation); 279 if (isLayoutRtl()) { 280 int width = getWidth(); 281 mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right; 282 mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left; 283 } 284 outline.setRoundRect( 285 mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f); 286 } 287 }); 288 289 // Layout Params for the Icon View 290 LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams(); 291 int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart; 292 orientationHandler.setIconAppChipChildrenParams( 293 iconParams, iconMarginStartRelativeToParent); 294 295 mIconView.setLayoutParams(iconParams); 296 mIconView.setDrawableSize(mAppIconSize, mAppIconSize); 297 298 // Layout Params for the collapsed Icon Text View 299 int textMarginStart = 300 iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin; 301 LayoutParams iconTextCollapsedParams = 302 (LayoutParams) mIconTextCollapsedView.getLayoutParams(); 303 orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart); 304 int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart 305 - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd; 306 iconTextCollapsedParams.width = collapsedTextWidth; 307 mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams); 308 mIconTextCollapsedView.setAlpha(1f); 309 310 // Layout Params for the expanded Icon Text View 311 LayoutParams iconTextExpandedParams = 312 (LayoutParams) mIconTextExpandedView.getLayoutParams(); 313 orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart); 314 mIconTextExpandedView.setLayoutParams(iconTextExpandedParams); 315 mIconTextExpandedView.setAlpha(0f); 316 mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth); 317 318 // Layout Params for the Icon Arrow View 319 LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams(); 320 int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize; 321 orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart); 322 mIconArrowView.setPivotY(iconArrowParams.height / 2f); 323 mIconArrowView.setLayoutParams(iconArrowParams); 324 325 // This method is called twice sometimes (like when rotating split tasks). It is called 326 // once before onMeasure and onLayout, and again after onMeasure but before onLayout with 327 // a new width. This happens because we update widths on rotation and on measure of 328 // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if 329 // it has just measured, so we explicitly call it here. 330 measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY), 331 MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY)); 332 } 333 334 @Override setIconColorTint(int color, float amount)335 public void setIconColorTint(int color, float amount) { 336 // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu. 337 float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR); 338 mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha); 339 } 340 341 @Override setContentAlpha(float alpha)342 public void setContentAlpha(float alpha) { 343 mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha); 344 } 345 346 @Override setModalAlpha(float alpha)347 public void setModalAlpha(float alpha) { 348 mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha); 349 } 350 351 @Override getDrawableWidth()352 public int getDrawableWidth() { 353 return mIconView == null ? 0 : mIconView.getDrawableWidth(); 354 } 355 356 @Override getDrawableHeight()357 public int getDrawableHeight() { 358 return mIconView == null ? 0 : mIconView.getDrawableHeight(); 359 } 360 revealAnim(boolean isRevealing)361 protected void revealAnim(boolean isRevealing) { 362 cancelInProgressAnimations(); 363 final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds(); 364 final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds(); 365 final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation); 366 mAnimator = new AnimatorSet(); 367 368 if (isRevealing) { 369 boolean isRtl = isLayoutRtl(); 370 bringToFront(); 371 // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu 372 Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal( 373 mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, 374 mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth()); 375 // Animate background clipping 376 ValueAnimator backgroundAnimator = ValueAnimator.ofObject( 377 mBackgroundAnimationRectEvaluator, 378 initialBackground, 379 expandedBackgroundBounds); 380 backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline()); 381 382 float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize; 383 float arrowTranslationX = 384 expandedBackgroundBounds.right - collapsedBackgroundBounds.right; 385 float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin; 386 float iconCenterToTextExpanded = 387 mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin; 388 float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed; 389 390 float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX; 391 float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX; 392 393 mAnimator.playTogether( 394 expandedTextRevealAnim, 395 backgroundAnimator, 396 ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling), 397 ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling), 398 ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 399 textTranslationXWithRtl), 400 ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 401 textTranslationXWithRtl), 402 ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0), 403 ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1), 404 ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl), 405 ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1)); 406 mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION); 407 } else { 408 // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu 409 Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal( 410 mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, 411 mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth()); 412 413 // Animate background clipping 414 ValueAnimator backgroundAnimator = ValueAnimator.ofObject( 415 mBackgroundAnimationRectEvaluator, 416 initialBackground, 417 collapsedBackgroundBounds); 418 backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline()); 419 420 mAnimator.playTogether( 421 expandedTextClipAnim, 422 backgroundAnimator, 423 ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1), 424 ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0), 425 ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0), 426 ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1), 427 ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0), 428 ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0), 429 ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1)); 430 mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION); 431 } 432 433 mAnimator.setInterpolator(EMPHASIZED); 434 mAnimator.start(); 435 } 436 getCollapsedBackgroundLtrBounds()437 private Rect getCollapsedBackgroundLtrBounds() { 438 Rect bounds = new Rect( 439 0, 440 0, 441 Math.min(mMaxWidth, mCollapsedMenuDefaultWidth), 442 mCollapsedMenuDefaultHeight); 443 bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart); 444 return bounds; 445 } 446 getExpandedBackgroundLtrBounds()447 private Rect getExpandedBackgroundLtrBounds() { 448 return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight); 449 } 450 cancelInProgressAnimations()451 private void cancelInProgressAnimations() { 452 // We null the `AnimatorSet` because it holds references to the `Animators` which aren't 453 // expecting to be mutable and will cause a crash if they are re-used. 454 if (mAnimator != null && mAnimator.isStarted()) { 455 mAnimator.cancel(); 456 mAnimator = null; 457 } 458 } 459 460 @Override asView()461 public View asView() { 462 return this; 463 } 464 } 465