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