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 PopupItemView(Context context)58 public PopupItemView(Context context) { 59 this(context, null, 0); 60 } 61 PopupItemView(Context context, AttributeSet attrs)62 public PopupItemView(Context context, AttributeSet attrs) { 63 this(context, attrs, 0); 64 } 65 PopupItemView(Context context, AttributeSet attrs, int defStyle)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 onFinishInflate()83 protected void onFinishInflate() { 84 super.onFinishInflate(); 85 mIconView = findViewById(R.id.popup_item_icon); 86 } 87 88 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)89 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 90 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 91 mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); 92 } 93 94 @Override dispatchDraw(Canvas canvas)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 */ createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft)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 onAnimationUpdate(ValueAnimator valueAnimator)137 public void onAnimationUpdate(ValueAnimator valueAnimator) { 138 mOpenAnimationProgress = valueAnimator.getAnimatedFraction(); 139 } 140 isOpenOrOpening()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 */ createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft, long duration)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 */ getIconCenter()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 getBackgroundRadius()182 protected float getBackgroundRadius() { 183 return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius); 184 } 185 getArrowColor(boolean isArrowAttachedToBottom)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 ZoomRevealOutlineProvider(int x, int y, Rect pillRect, PopupItemView translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft, float arrowCenter)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 setProgress(float progress)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 */ CloseInterpolator(float openAnimationProgress)245 public CloseInterpolator(float openAnimationProgress) { 246 super(100, 0); 247 mStartProgress = 1f - openAnimationProgress; 248 mRemainingProgress = openAnimationProgress; 249 } 250 251 @Override getInterpolation(float v)252 public float getInterpolation(float v) { 253 return mStartProgress + super.getInterpolation(v) * mRemainingProgress; 254 } 255 } 256 } 257