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