1 /* 2 * Copyright (C) 2015 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.messaging.ui.animation; 18 19 import android.animation.TypeEvaluator; 20 import android.app.Activity; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Rect; 24 import android.view.Gravity; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.view.animation.Animation; 28 import android.view.animation.Transformation; 29 import android.widget.PopupWindow; 30 31 import com.android.messaging.util.LogUtil; 32 import com.android.messaging.util.ThreadUtil; 33 import com.android.messaging.util.UiUtils; 34 35 /** 36 * Animates viewToAnimate from startRect to the place where it is in the layout, viewToAnimate 37 * should be in its final destination location before startAfterLayoutComplete is called. 38 * viewToAnimate will be drawn scaled and offset in a popupWindow. 39 * This class handles the case where the viewToAnimate moves during the animation 40 */ 41 public class PopupTransitionAnimation extends Animation { 42 /** The view we're animating */ 43 private final View mViewToAnimate; 44 45 /** The rect to start the slide in animation from */ 46 private final Rect mStartRect; 47 48 /** The rect of the currently animated view */ 49 private Rect mCurrentRect; 50 51 /** The rect that we're animating to. This can change during the animation */ 52 private final Rect mDestRect; 53 54 /** The bounds of the popup in window coordinates. Does not include notification bar */ 55 private final Rect mPopupRect; 56 57 /** The bounds of the action bar in window coordinates. We clip the popup to below this */ 58 private final Rect mActionBarRect; 59 60 /** Interpolates between the start and end rect for every animation tick */ 61 private final TypeEvaluator<Rect> mRectEvaluator; 62 63 /** The popup window that holds contains the animating view */ 64 private PopupWindow mPopupWindow; 65 66 /** The layout root for the popup which is where the animated view is rendered */ 67 private View mPopupRoot; 68 69 /** The action bar's view */ 70 private final View mActionBarView; 71 72 private Runnable mOnStartCallback; 73 private Runnable mOnStopCallback; 74 PopupTransitionAnimation(final Rect startRect, final View viewToAnimate)75 public PopupTransitionAnimation(final Rect startRect, final View viewToAnimate) { 76 mViewToAnimate = viewToAnimate; 77 mStartRect = startRect; 78 mCurrentRect = new Rect(mStartRect); 79 mDestRect = new Rect(); 80 mPopupRect = new Rect(); 81 mActionBarRect = new Rect(); 82 mActionBarView = viewToAnimate.getRootView().findViewById( 83 androidx.appcompat.R.id.action_bar); 84 mRectEvaluator = RectEvaluatorCompat.create(); 85 setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION); 86 setInterpolator(UiUtils.DEFAULT_INTERPOLATOR); 87 setAnimationListener(new AnimationListener() { 88 @Override 89 public void onAnimationStart(final Animation animation) { 90 if (mOnStartCallback != null) { 91 mOnStartCallback.run(); 92 } 93 mEvents.append("oAS,"); 94 } 95 96 @Override 97 public void onAnimationEnd(final Animation animation) { 98 if (mOnStopCallback != null) { 99 mOnStopCallback.run(); 100 } 101 dismiss(); 102 mEvents.append("oAE,"); 103 } 104 105 @Override 106 public void onAnimationRepeat(final Animation animation) { 107 } 108 }); 109 } 110 111 private final StringBuilder mEvents = new StringBuilder(); 112 private final Runnable mCleanupRunnable = new Runnable() { 113 @Override 114 public void run() { 115 LogUtil.w(LogUtil.BUGLE_TAG, "PopupTransitionAnimation: " + mEvents); 116 } 117 }; 118 119 /** 120 * Ensures the animation is ready before starting the animation. 121 * viewToAnimate must first be layed out so we know where we will animate to 122 */ startAfterLayoutComplete()123 public void startAfterLayoutComplete() { 124 // We want layout to occur, and then we immediately animate it in, so hide it initially to 125 // reduce jank on the first frame 126 mViewToAnimate.setVisibility(View.INVISIBLE); 127 mViewToAnimate.setAlpha(0); 128 129 final Runnable startAnimation = new Runnable() { 130 boolean mRunComplete = false; 131 boolean mFirstTry = true; 132 133 @Override 134 public void run() { 135 if (mRunComplete) { 136 return; 137 } 138 139 mViewToAnimate.getGlobalVisibleRect(mDestRect); 140 // In Android views which are visible but haven't computed their size yet have a 141 // size of 1x1 because anything with a size of 0x0 is considered hidden. We can't 142 // start the animation until after the size is greater than 1x1 143 if (mDestRect.width() <= 1 || mDestRect.height() <= 1) { 144 // Layout hasn't occurred yet 145 if (!mFirstTry) { 146 // Give up if this is not the first try, since layout change still doesn't 147 // yield a size for the view. This is likely because the media picker is 148 // full screen so there's no space left for the animated view. We give up 149 // on animation, but need to make sure the view that was initially 150 // hidden is re-shown. 151 mViewToAnimate.setAlpha(1); 152 mViewToAnimate.setVisibility(View.VISIBLE); 153 } else { 154 mFirstTry = false; 155 UiUtils.doOnceAfterLayoutChange(mViewToAnimate, this); 156 } 157 return; 158 } 159 160 mRunComplete = true; 161 mViewToAnimate.startAnimation(PopupTransitionAnimation.this); 162 mViewToAnimate.invalidate(); 163 // http://b/20856505: The PopupWindow sometimes does not get dismissed. 164 ThreadUtil.getMainThreadHandler().postDelayed(mCleanupRunnable, getDuration() * 2); 165 } 166 }; 167 168 startAnimation.run(); 169 } 170 setOnStartCallback(final Runnable onStart)171 public PopupTransitionAnimation setOnStartCallback(final Runnable onStart) { 172 mOnStartCallback = onStart; 173 return this; 174 } 175 setOnStopCallback(final Runnable onStop)176 public PopupTransitionAnimation setOnStopCallback(final Runnable onStop) { 177 mOnStopCallback = onStop; 178 return this; 179 } 180 181 @Override applyTransformation(final float interpolatedTime, final Transformation t)182 protected void applyTransformation(final float interpolatedTime, final Transformation t) { 183 if (mPopupWindow == null) { 184 initPopupWindow(); 185 } 186 // Update mDestRect as it may have moved during the animation 187 mPopupRect.set(UiUtils.getMeasuredBoundsOnScreen(mPopupRoot)); 188 mActionBarRect.set(UiUtils.getMeasuredBoundsOnScreen(mActionBarView)); 189 computeDestRect(); 190 191 // Update currentRect to the new animated coordinates, and request mPopupRoot to redraw 192 // itself at the new coordinates 193 mCurrentRect = mRectEvaluator.evaluate(interpolatedTime, mStartRect, mDestRect); 194 mPopupRoot.invalidate(); 195 196 if (interpolatedTime >= 0.98) { 197 mEvents.append("aT").append(interpolatedTime).append(','); 198 } 199 if (interpolatedTime == 1) { 200 dismiss(); 201 } 202 } 203 dismiss()204 private void dismiss() { 205 mEvents.append("d,"); 206 mViewToAnimate.setAlpha(1); 207 mViewToAnimate.setVisibility(View.VISIBLE); 208 // Delay dismissing the popup window to let mViewToAnimate draw under it and reduce the 209 // flash 210 ThreadUtil.getMainThreadHandler().post(new Runnable() { 211 @Override 212 public void run() { 213 try { 214 mPopupWindow.dismiss(); 215 } catch (IllegalArgumentException e) { 216 // PopupWindow.dismiss() will fire an IllegalArgumentException if the activity 217 // has already ended while we were animating 218 } 219 ThreadUtil.getMainThreadHandler().removeCallbacks(mCleanupRunnable); 220 } 221 }); 222 } 223 224 @Override willChangeBounds()225 public boolean willChangeBounds() { 226 return false; 227 } 228 229 /** 230 * Computes mDestRect (the position in window space of the placeholder view that we should 231 * animate to). Some frames during the animation fail to compute getGlobalVisibleRect, so use 232 * the last known values in that case 233 */ computeDestRect()234 private void computeDestRect() { 235 final int prevTop = mDestRect.top; 236 final int prevLeft = mDestRect.left; 237 final int prevRight = mDestRect.right; 238 final int prevBottom = mDestRect.bottom; 239 240 if (!getViewScreenMeasureRect(mViewToAnimate, mDestRect)) { 241 mDestRect.top = prevTop; 242 mDestRect.left = prevLeft; 243 mDestRect.bottom = prevBottom; 244 mDestRect.right = prevRight; 245 } 246 } 247 248 /** 249 * Sets up the PopupWindow that the view will animate in. Animating the size and position of a 250 * popup can be choppy, so instead we make the popup fill the entire space of the screen, and 251 * animate the position of viewToAnimate within the popup using a Transformation 252 */ initPopupWindow()253 private void initPopupWindow() { 254 mPopupRoot = new View(mViewToAnimate.getContext()) { 255 @Override 256 protected void onDraw(final Canvas canvas) { 257 canvas.save(); 258 canvas.clipRect(getLeft(), mActionBarRect.bottom - mPopupRect.top, getRight(), 259 getBottom()); 260 canvas.drawColor(Color.TRANSPARENT); 261 final float previousAlpha = mViewToAnimate.getAlpha(); 262 mViewToAnimate.setAlpha(1); 263 // The view's global position includes the notification bar height, but 264 // the popup window may or may not cover the notification bar (depending on screen 265 // rotation, IME status etc.), so we need to compensate for this difference by 266 // offseting vertically. 267 canvas.translate(mCurrentRect.left, mCurrentRect.top - mPopupRect.top); 268 269 final float viewWidth = mViewToAnimate.getWidth(); 270 final float viewHeight = mViewToAnimate.getHeight(); 271 if (viewWidth > 0 && viewHeight > 0) { 272 canvas.scale(mCurrentRect.width() / viewWidth, 273 mCurrentRect.height() / viewHeight); 274 } 275 canvas.clipRect(0, 0, mCurrentRect.width(), mCurrentRect.height()); 276 if (!mPopupRect.isEmpty()) { 277 // HACK: Layout is unstable until mPopupRect is non-empty. 278 mViewToAnimate.draw(canvas); 279 } 280 mViewToAnimate.setAlpha(previousAlpha); 281 canvas.restore(); 282 } 283 }; 284 mPopupWindow = new PopupWindow(mViewToAnimate.getContext()); 285 mPopupWindow.setBackgroundDrawable(null); 286 mPopupWindow.setContentView(mPopupRoot); 287 mPopupWindow.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); 288 mPopupWindow.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); 289 mPopupWindow.setTouchable(false); 290 // We must pass a non-zero value for the y offset, or else the system resets the status bar 291 // color to black (M only) during the animation. The actual position of the window (and 292 // the animated view inside it) are still correct, regardless of what we pass for the y 293 // parameter (e.g. 1 and 100 both work). Not entirely sure why this works. 294 mPopupWindow.showAtLocation(mViewToAnimate, Gravity.TOP, 0, 1); 295 } 296 getViewScreenMeasureRect(final View view, final Rect outRect)297 private static boolean getViewScreenMeasureRect(final View view, final Rect outRect) { 298 outRect.set(UiUtils.getMeasuredBoundsOnScreen(view)); 299 return !outRect.isEmpty(); 300 } 301 } 302