Home | History | Annotate | Download | only in popup
      1 /*
      2  * Copyright (C) 2017 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.ValueAnimator;
     22 import android.content.Context;
     23 import android.graphics.Bitmap;
     24 import android.graphics.Canvas;
     25 import android.graphics.Matrix;
     26 import android.graphics.Paint;
     27 import android.graphics.Point;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.PorterDuffXfermode;
     30 import android.graphics.Rect;
     31 import android.util.AttributeSet;
     32 import android.view.View;
     33 import android.widget.FrameLayout;
     34 
     35 import com.android.launcher3.LogAccelerateInterpolator;
     36 import com.android.launcher3.R;
     37 import com.android.launcher3.Utilities;
     38 import com.android.launcher3.util.PillRevealOutlineProvider;
     39 
     40 /**
     41  * An abstract {@link FrameLayout} that supports animating an item's content
     42  * (e.g. icon and text) separate from the item's background.
     43  */
     44 public abstract class PopupItemView extends FrameLayout
     45         implements ValueAnimator.AnimatorUpdateListener {
     46 
     47     protected static final Point sTempPoint = new Point();
     48 
     49     protected final Rect mPillRect;
     50     private float mOpenAnimationProgress;
     51     protected final boolean mIsRtl;
     52     protected View mIconView;
     53 
     54     private final Paint mBackgroundClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
     55     private final Matrix mMatrix = new Matrix();
     56     private Bitmap mRoundedCornerBitmap;
     57 
     58     public PopupItemView(Context context) {
     59         this(context, null, 0);
     60     }
     61 
     62     public PopupItemView(Context context, AttributeSet attrs) {
     63         this(context, attrs, 0);
     64     }
     65 
     66     public PopupItemView(Context context, AttributeSet attrs, int defStyle) {
     67         super(context, attrs, defStyle);
     68 
     69         mPillRect = new Rect();
     70 
     71         // Initialize corner clipping Bitmap and Paint.
     72         int radius = (int) getBackgroundRadius();
     73         mRoundedCornerBitmap = Bitmap.createBitmap(radius, radius, Bitmap.Config.ALPHA_8);
     74         Canvas canvas = new Canvas();
     75         canvas.setBitmap(mRoundedCornerBitmap);
     76         canvas.drawArc(0, 0, radius*2, radius*2, 180, 90, true, mBackgroundClipPaint);
     77         mBackgroundClipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
     78 
     79         mIsRtl = Utilities.isRtl(getResources());
     80     }
     81 
     82     @Override
     83     protected void onFinishInflate() {
     84         super.onFinishInflate();
     85         mIconView = findViewById(R.id.popup_item_icon);
     86     }
     87 
     88     @Override
     89     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     90         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     91         mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
     92     }
     93 
     94     @Override
     95     protected void dispatchDraw(Canvas canvas) {
     96         int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
     97         super.dispatchDraw(canvas);
     98 
     99         int cornerWidth = mRoundedCornerBitmap.getWidth();
    100         int cornerHeight = mRoundedCornerBitmap.getHeight();
    101         // Clip top left corner.
    102         mMatrix.reset();
    103         canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
    104         // Clip top right corner.
    105         mMatrix.setRotate(90, cornerWidth / 2, cornerHeight / 2);
    106         mMatrix.postTranslate(canvas.getWidth() - cornerWidth, 0);
    107         canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
    108         // Clip bottom right corner.
    109         mMatrix.setRotate(180, cornerWidth / 2, cornerHeight / 2);
    110         mMatrix.postTranslate(canvas.getWidth() - cornerWidth, canvas.getHeight() - cornerHeight);
    111         canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
    112         // Clip bottom left corner.
    113         mMatrix.setRotate(270, cornerWidth / 2, cornerHeight / 2);
    114         mMatrix.postTranslate(0, canvas.getHeight() - cornerHeight);
    115         canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
    116 
    117         canvas.restoreToCount(saveCount);
    118     }
    119 
    120     /**
    121      * Creates an animator to play when the shortcut container is being opened.
    122      */
    123     public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) {
    124         Point center = getIconCenter();
    125         int arrowCenter = getResources().getDimensionPixelSize(pivotLeft ^ mIsRtl ?
    126                 R.dimen.popup_arrow_horizontal_center_start:
    127                 R.dimen.popup_arrow_horizontal_center_end);
    128         ValueAnimator openAnimator =  new ZoomRevealOutlineProvider(center.x, center.y,
    129                 mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft, arrowCenter)
    130                         .createRevealAnimator(this, false);
    131         mOpenAnimationProgress = 0f;
    132         openAnimator.addUpdateListener(this);
    133         return openAnimator;
    134     }
    135 
    136     @Override
    137     public void onAnimationUpdate(ValueAnimator valueAnimator) {
    138         mOpenAnimationProgress = valueAnimator.getAnimatedFraction();
    139     }
    140 
    141     public boolean isOpenOrOpening() {
    142         return mOpenAnimationProgress > 0;
    143     }
    144 
    145     /**
    146      * Creates an animator to play when the shortcut container is being closed.
    147      */
    148     public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft,
    149             long duration) {
    150         Point center = getIconCenter();
    151         int arrowCenter = getResources().getDimensionPixelSize(pivotLeft ^ mIsRtl ?
    152                 R.dimen.popup_arrow_horizontal_center_start :
    153                 R.dimen.popup_arrow_horizontal_center_end);
    154         ValueAnimator closeAnimator = new ZoomRevealOutlineProvider(center.x, center.y,
    155                 mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft, arrowCenter)
    156                         .createRevealAnimator(this, true);
    157         // Scale down the duration and interpolator according to the progress
    158         // that the open animation was at when the close started.
    159         closeAnimator.setDuration((long) (duration * mOpenAnimationProgress));
    160         closeAnimator.setInterpolator(new CloseInterpolator(mOpenAnimationProgress));
    161         closeAnimator.addListener(new AnimatorListenerAdapter() {
    162             @Override
    163             public void onAnimationEnd(Animator animation) {
    164                 mOpenAnimationProgress = 0;
    165             }
    166         });
    167         return closeAnimator;
    168     }
    169 
    170     /**
    171      * Returns the position of the center of the icon relative to the container.
    172      */
    173     public Point getIconCenter() {
    174         sTempPoint.y = getMeasuredHeight() / 2;
    175         sTempPoint.x = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height) / 2;
    176         if (Utilities.isRtl(getResources())) {
    177             sTempPoint.x = getMeasuredWidth() - sTempPoint.x;
    178         }
    179         return sTempPoint;
    180     }
    181 
    182     protected float getBackgroundRadius() {
    183         return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius);
    184     }
    185 
    186     public abstract int getArrowColor(boolean isArrowAttachedToBottom);
    187 
    188     /**
    189      * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height.
    190      */
    191     private static class ZoomRevealOutlineProvider extends PillRevealOutlineProvider {
    192 
    193         private final View mTranslateView;
    194         private final View mZoomView;
    195 
    196         private final float mFullHeight;
    197         private final float mTranslateYMultiplier;
    198 
    199         private final boolean mPivotLeft;
    200         private final float mTranslateX;
    201         private final float mArrowCenter;
    202 
    203         public ZoomRevealOutlineProvider(int x, int y, Rect pillRect, PopupItemView translateView,
    204                 View zoomView, boolean isContainerAboveIcon, boolean pivotLeft, float arrowCenter) {
    205             super(x, y, pillRect, translateView.getBackgroundRadius());
    206             mTranslateView = translateView;
    207             mZoomView = zoomView;
    208             mFullHeight = pillRect.height();
    209 
    210             mTranslateYMultiplier = isContainerAboveIcon ? 0.5f : -0.5f;
    211 
    212             mPivotLeft = pivotLeft;
    213             mTranslateX = pivotLeft ? arrowCenter : pillRect.right - arrowCenter;
    214             mArrowCenter = arrowCenter;
    215         }
    216 
    217         @Override
    218         public void setProgress(float progress) {
    219             super.setProgress(progress);
    220 
    221             if (mZoomView != null) {
    222                 mZoomView.setScaleX(progress);
    223                 mZoomView.setScaleY(progress);
    224             }
    225 
    226             float height = mOutline.height();
    227             mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height));
    228 
    229             float offsetX = Math.min(mOutline.width(), mArrowCenter);
    230             float pivotX = mPivotLeft ? (mOutline.left + offsetX) : (mOutline.right - offsetX);
    231             mTranslateView.setTranslationX(mTranslateX - pivotX);
    232         }
    233     }
    234 
    235     /**
    236      * An interpolator that reverses the current open animation progress.
    237      */
    238     private static class CloseInterpolator extends LogAccelerateInterpolator {
    239         private float mStartProgress;
    240         private float mRemainingProgress;
    241 
    242         /**
    243          * @param openAnimationProgress The progress that the open interpolator ended at.
    244          */
    245         public CloseInterpolator(float openAnimationProgress) {
    246             super(100, 0);
    247             mStartProgress = 1f - openAnimationProgress;
    248             mRemainingProgress = openAnimationProgress;
    249         }
    250 
    251         @Override
    252         public float getInterpolation(float v) {
    253             return mStartProgress + super.getInterpolation(v) * mRemainingProgress;
    254         }
    255     }
    256 }
    257