Home | History | Annotate | Download | only in popup
      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 
     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 
    102     public ArrowPopup(Context context, AttributeSet attrs) {
    103         this(context, attrs, 0);
    104     }
    105 
    106     public ArrowPopup(Context context) {
    107         this(context, null, 0);
    108     }
    109 
    110     @Override
    111     protected void handleClose(boolean animate) {
    112         if (animate) {
    113             animateClose();
    114         } else {
    115             closeComplete();
    116         }
    117     }
    118 
    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      */
    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      */
    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 
    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      */
    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      */
    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
    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 
    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 
    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      */
    431     protected void onCreateCloseAnimation(AnimatorSet anim) { }
    432 
    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      */
    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