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