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