• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.launcher3.popup;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.TimeInterpolator;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.graphics.CornerPathEffect;
28 import android.graphics.Outline;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.graphics.drawable.ShapeDrawable;
32 import android.util.AttributeSet;
33 import android.view.Gravity;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.ViewOutlineProvider;
38 import android.view.animation.AccelerateDecelerateInterpolator;
39 
40 import com.android.launcher3.AbstractFloatingView;
41 import com.android.launcher3.Launcher;
42 import com.android.launcher3.LauncherAnimUtils;
43 import com.android.launcher3.R;
44 import com.android.launcher3.Utilities;
45 import com.android.launcher3.anim.RevealOutlineAnimation;
46 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
47 import com.android.launcher3.dragndrop.DragLayer;
48 import com.android.launcher3.graphics.TriangleShape;
49 import com.android.launcher3.util.Themes;
50 
51 import java.util.ArrayList;
52 import java.util.Collections;
53 
54 /**
55  * A container for shortcuts to deep links and notifications associated with an app.
56  */
57 public abstract class ArrowPopup extends AbstractFloatingView {
58 
59     private final Rect mTempRect = new Rect();
60 
61     protected final LayoutInflater mInflater;
62     private final float mOutlineRadius;
63     protected final Launcher mLauncher;
64     protected final boolean mIsRtl;
65 
66     private final int mArrayOffset;
67     private final View mArrow;
68 
69     protected boolean mIsLeftAligned;
70     protected boolean mIsAboveIcon;
71     private int mGravity;
72 
73     protected Animator mOpenCloseAnimator;
74     protected boolean mDeferContainerRemoval;
75     private final Rect mStartRect = new Rect();
76     private final Rect mEndRect = new Rect();
77 
ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr)78     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
79         super(context, attrs, defStyleAttr);
80         mInflater = LayoutInflater.from(context);
81         mOutlineRadius = getResources().getDimension(R.dimen.bg_round_rect_radius);
82         mLauncher = Launcher.getLauncher(context);
83         mIsRtl = Utilities.isRtl(getResources());
84 
85         setClipToOutline(true);
86         setOutlineProvider(new ViewOutlineProvider() {
87             @Override
88             public void getOutline(View view, Outline outline) {
89                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
90             }
91         });
92 
93         // Initialize arrow view
94         final Resources resources = getResources();
95         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
96         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
97         mArrow = new View(context);
98         mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight));
99         mArrayOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
100     }
101 
ArrowPopup(Context context, AttributeSet attrs)102     public ArrowPopup(Context context, AttributeSet attrs) {
103         this(context, attrs, 0);
104     }
105 
ArrowPopup(Context context)106     public ArrowPopup(Context context) {
107         this(context, null, 0);
108     }
109 
110     @Override
handleClose(boolean animate)111     protected void handleClose(boolean animate) {
112         if (animate) {
113             animateClose();
114         } else {
115             closeComplete();
116         }
117     }
118 
inflateAndAdd(int resId, ViewGroup container)119     public <T extends View> T inflateAndAdd(int resId, ViewGroup container) {
120         View view = mInflater.inflate(resId, container, false);
121         container.addView(view);
122         return (T) view;
123     }
124 
125     /**
126      * Called when all view inflation and reordering in complete.
127      */
onInflationComplete(boolean isReversed)128     protected void onInflationComplete(boolean isReversed) { }
129 
130     /**
131      * Shows the popup at the desired location, optionally reversing the children.
132      * @param viewsToFlip number of views from the top to to flip in case of reverse order
133      */
reorderAndShow(int viewsToFlip)134     protected void reorderAndShow(int viewsToFlip) {
135         setVisibility(View.INVISIBLE);
136         mIsOpen = true;
137         mLauncher.getDragLayer().addView(this);
138         orientAboutObject();
139 
140         boolean reverseOrder = mIsAboveIcon;
141         if (reverseOrder) {
142             int count = getChildCount();
143             ArrayList<View> allViews = new ArrayList<>(count);
144             for (int i = 0; i < count; i++) {
145                 if (i == viewsToFlip) {
146                     Collections.reverse(allViews);
147                 }
148                 allViews.add(getChildAt(i));
149             }
150             Collections.reverse(allViews);
151             removeAllViews();
152             for (int i = 0; i < count; i++) {
153                 addView(allViews.get(i));
154             }
155 
156             orientAboutObject();
157         }
158         onInflationComplete(reverseOrder);
159 
160         // Add the arrow.
161         final Resources res = getResources();
162         final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
163                 ? R.dimen.popup_arrow_horizontal_center_start
164                 : R.dimen.popup_arrow_horizontal_center_end);
165         final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
166         mLauncher.getDragLayer().addView(mArrow);
167         DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
168         if (mIsLeftAligned) {
169             mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
170         } else {
171             mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
172         }
173 
174         if (Gravity.isVertical(mGravity)) {
175             // This is only true if there wasn't room for the container next to the icon,
176             // so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
177             mArrow.setVisibility(INVISIBLE);
178         } else {
179             ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
180                     arrowLp.width, arrowLp.height, !mIsAboveIcon));
181             Paint arrowPaint = arrowDrawable.getPaint();
182             arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary));
183             // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
184             int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
185             arrowPaint.setPathEffect(new CornerPathEffect(radius));
186             mArrow.setBackground(arrowDrawable);
187             mArrow.setElevation(getElevation());
188         }
189 
190         mArrow.setPivotX(arrowLp.width / 2);
191         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowLp.height);
192 
193         animateOpen();
194     }
195 
isAlignedWithStart()196     protected boolean isAlignedWithStart() {
197         return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
198     }
199 
200     /**
201      * Provide the location of the target object relative to the dragLayer.
202      */
getTargetObjectLocation(Rect outPos)203     protected abstract void getTargetObjectLocation(Rect outPos);
204 
205     /**
206      * Orients this container above or below the given icon, aligning with the left or right.
207      *
208      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
209      * - Above and left-aligned
210      * - Above and right-aligned
211      * - Below and left-aligned
212      * - Below and right-aligned
213      *
214      * So we always align left if there is enough horizontal space
215      * and align above if there is enough vertical space.
216      */
orientAboutObject()217     protected void orientAboutObject() {
218         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
219         int width = getMeasuredWidth();
220         int extraVerticalSpace = mArrow.getLayoutParams().height + mArrayOffset
221                 + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
222         int height = getMeasuredHeight() + extraVerticalSpace;
223 
224         getTargetObjectLocation(mTempRect);
225         DragLayer dragLayer = mLauncher.getDragLayer();
226         Rect insets = dragLayer.getInsets();
227 
228         // Align left (right in RTL) if there is room.
229         int leftAlignedX = mTempRect.left;
230         int rightAlignedX = mTempRect.right - width;
231         int x = leftAlignedX;
232         boolean canBeLeftAligned = leftAlignedX + width + insets.left
233                 < dragLayer.getRight() - insets.right;
234         boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
235         if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
236             x = rightAlignedX;
237         }
238         mIsLeftAligned = x == leftAlignedX;
239 
240         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
241         int iconWidth = mTempRect.width();
242         Resources resources = getResources();
243         int xOffset;
244         if (isAlignedWithStart()) {
245             // Aligning with the shortcut icon.
246             int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
247             int shortcutPaddingStart = resources.getDimensionPixelSize(
248                     R.dimen.popup_padding_start);
249             xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
250         } else {
251             // Aligning with the drag handle.
252             int shortcutDragHandleWidth = resources.getDimensionPixelSize(
253                     R.dimen.deep_shortcut_drag_handle_size);
254             int shortcutPaddingEnd = resources.getDimensionPixelSize(
255                     R.dimen.popup_padding_end);
256             xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
257         }
258         x += mIsLeftAligned ? xOffset : -xOffset;
259 
260         // Open above icon if there is room.
261         int iconHeight = mTempRect.height();
262         int y = mTempRect.top - height;
263         mIsAboveIcon = y > dragLayer.getTop() + insets.top;
264         if (!mIsAboveIcon) {
265             y = mTempRect.top + iconHeight + extraVerticalSpace;
266         }
267 
268         // Insets are added later, so subtract them now.
269         if (mIsRtl) {
270             x += insets.right;
271         } else {
272             x -= insets.left;
273         }
274         y -= insets.top;
275 
276         mGravity = 0;
277         if (y + height > dragLayer.getBottom() - insets.bottom) {
278             // The container is opening off the screen, so just center it in the drag layer instead.
279             mGravity = Gravity.CENTER_VERTICAL;
280             // Put the container next to the icon, preferring the right side in ltr (left in rtl).
281             int rightSide = leftAlignedX + iconWidth - insets.left;
282             int leftSide = rightAlignedX - iconWidth - insets.left;
283             if (!mIsRtl) {
284                 if (rightSide + width < dragLayer.getRight()) {
285                     x = rightSide;
286                     mIsLeftAligned = true;
287                 } else {
288                     x = leftSide;
289                     mIsLeftAligned = false;
290                 }
291             } else {
292                 if (leftSide > dragLayer.getLeft()) {
293                     x = leftSide;
294                     mIsLeftAligned = false;
295                 } else {
296                     x = rightSide;
297                     mIsLeftAligned = true;
298                 }
299             }
300             mIsAboveIcon = true;
301         }
302 
303         setX(x);
304         if (Gravity.isVertical(mGravity)) {
305             return;
306         }
307 
308         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
309         DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
310         if (mIsAboveIcon) {
311             arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
312             lp.bottomMargin =
313                     mLauncher.getDragLayer().getHeight() - y - getMeasuredHeight() - insets.top;
314             arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrayOffset - insets.bottom;
315         } else {
316             arrowLp.gravity = lp.gravity = Gravity.TOP;
317             lp.topMargin = y + insets.top;
318             arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrayOffset;
319         }
320     }
321 
322     @Override
onLayout(boolean changed, int l, int t, int r, int b)323     protected void onLayout(boolean changed, int l, int t, int r, int b) {
324         super.onLayout(changed, l, t, r, b);
325 
326         // enforce contained is within screen
327         DragLayer dragLayer = mLauncher.getDragLayer();
328         if (getTranslationX() + l < 0 || getTranslationX() + r > dragLayer.getWidth()) {
329             // If we are still off screen, center horizontally too.
330             mGravity |= Gravity.CENTER_HORIZONTAL;
331         }
332 
333         if (Gravity.isHorizontal(mGravity)) {
334             setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
335             mArrow.setVisibility(INVISIBLE);
336         }
337         if (Gravity.isVertical(mGravity)) {
338             setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
339         }
340     }
341 
animateOpen()342     private void animateOpen() {
343         setVisibility(View.VISIBLE);
344 
345         final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
346         final Resources res = getResources();
347         final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
348         final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
349 
350         // Rectangular reveal.
351         final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
352                 .createRevealAnimator(this, false);
353         revealAnim.setDuration(revealDuration);
354         revealAnim.setInterpolator(revealInterpolator);
355 
356         Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1);
357         fadeIn.setDuration(revealDuration);
358         fadeIn.setInterpolator(revealInterpolator);
359         openAnim.play(fadeIn);
360 
361         // Animate the arrow.
362         mArrow.setScaleX(0);
363         mArrow.setScaleY(0);
364         Animator arrowScale = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 1)
365                 .setDuration(res.getInteger(R.integer.config_popupArrowOpenDuration));
366 
367         openAnim.addListener(new AnimatorListenerAdapter() {
368             @Override
369             public void onAnimationEnd(Animator animation) {
370                 announceAccessibilityChanges();
371                 mOpenCloseAnimator = null;
372             }
373         });
374 
375         mOpenCloseAnimator = openAnim;
376         openAnim.playSequentially(revealAnim, arrowScale);
377         openAnim.start();
378     }
379 
animateClose()380     protected void animateClose() {
381         if (!mIsOpen) {
382             return;
383         }
384         mEndRect.setEmpty();
385         if (getOutlineProvider() instanceof RevealOutlineAnimation) {
386             ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
387         }
388         if (mOpenCloseAnimator != null) {
389             mOpenCloseAnimator.cancel();
390         }
391         mIsOpen = false;
392 
393         final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
394         // Hide the arrow
395         closeAnim.play(ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0));
396         closeAnim.play(ObjectAnimator.ofFloat(mArrow, ALPHA, 0));
397 
398         final Resources res = getResources();
399         final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
400 
401         // Rectangular reveal (reversed).
402         final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
403                 .createRevealAnimator(this, true);
404         revealAnim.setInterpolator(revealInterpolator);
405         closeAnim.play(revealAnim);
406 
407         Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
408         fadeOut.setInterpolator(revealInterpolator);
409         closeAnim.play(fadeOut);
410 
411         onCreateCloseAnimation(closeAnim);
412         closeAnim.setDuration((long) res.getInteger(R.integer.config_popupOpenCloseDuration));
413         closeAnim.addListener(new AnimatorListenerAdapter() {
414             @Override
415             public void onAnimationEnd(Animator animation) {
416                 mOpenCloseAnimator = null;
417                 if (mDeferContainerRemoval) {
418                     setVisibility(INVISIBLE);
419                 } else {
420                     closeComplete();
421                 }
422             }
423         });
424         mOpenCloseAnimator = closeAnim;
425         closeAnim.start();
426     }
427 
428     /**
429      * Called when creating the close transition allowing subclass can add additional animations.
430      */
onCreateCloseAnimation(AnimatorSet anim)431     protected void onCreateCloseAnimation(AnimatorSet anim) { }
432 
createOpenCloseOutlineProvider()433     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
434         int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
435                 R.dimen.popup_arrow_horizontal_center_start:
436                 R.dimen.popup_arrow_horizontal_center_end);
437         if (!mIsLeftAligned) {
438             arrowCenterX = getMeasuredWidth() - arrowCenterX;
439         }
440         int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;
441 
442         mStartRect.set(arrowCenterX, arrowCenterY, arrowCenterX, arrowCenterY);
443         if (mEndRect.isEmpty()) {
444             mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
445         }
446 
447         return new RoundedRectRevealOutlineProvider
448                 (mOutlineRadius, mOutlineRadius, mStartRect, mEndRect);
449     }
450 
451     /**
452      * Closes the popup without animation.
453      */
closeComplete()454     protected void closeComplete() {
455         if (mOpenCloseAnimator != null) {
456             mOpenCloseAnimator.cancel();
457             mOpenCloseAnimator = null;
458         }
459         mIsOpen = false;
460         mDeferContainerRemoval = false;
461         mLauncher.getDragLayer().removeView(this);
462         mLauncher.getDragLayer().removeView(mArrow);
463     }
464 }
465